While döngüsü içinde değiştirilen bir değişken hatırlanmaz


187

Aşağıdaki programda, değişkeni $fooilk ififadenin içindeki 1 değerine ayarlarsam , if ifadesinden sonra değerinin hatırlanması anlamında çalışır. Ancak, aynı değişkeni ifbir whiledeyimin içindeki bir içindeki 2 değerine ayarladığımda , whiledöngüden sonra unutulur . Döngü $fooiçinde değişkenin bir kopyasını kullanıyorum gibi davranıyorum whileve sadece o kopyayı değiştiriyorum. İşte tam bir test programı:

#!/bin/bash

set -e
set -u 
foo=0
bar="hello"  
if [[ "$bar" == "hello" ]]
then
    foo=1
    echo "Setting \$foo to 1: $foo"
fi

echo "Variable \$foo after if statement: $foo"   
lines="first line\nsecond line\nthird line" 
echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2
    echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done

echo "Variable \$foo after while loop: $foo"

# Output:
# $ ./testbash.sh
# Setting $foo to 1: 1
# Variable $foo after if statement: 1
# Value of $foo in while loop body: 1
# Variable $foo updated to 2 inside if inside while loop
# Value of $foo in while loop body: 2
# Value of $foo in while loop body: 2
# Variable $foo after while loop: 1

# bash --version
# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)


Shellcheck yardımcı programı bunu yakalar (bkz. Github.com/koalaman/shellcheck/wiki/SC2030 ); Yukarıdaki kodu shellcheck.net'te kesip yapıştırmak, Hat 19 için bu geri bildirimi yayınlar:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Yanıtlar:


244
echo -e $lines | while read line 
    ...
done

whileDöngü bir kabuktaki yürütülür. Bu nedenle, alt kabuk çıktıktan sonra değişken üzerinde yaptığınız hiçbir değişiklik kullanılamaz.

Bunun yerine, burada döngüyü ana kabuk işleminde olacak şekilde yeniden yazmak için burada bir dize kullanabilirsiniz ; yalnızca echo -e $linesbir alt kabukta çalışır:

while read line
do
    if [[ "$line" == "second line" ]]
    then
        foo=2
        echo "Variable \$foo updated to $foo inside if inside while loop"
    fi
    echo "Value of \$foo in while loop body: $foo"
done <<< "$(echo -e "$lines")"

echoAtama sırasında hemen ters eğik çizgi dizilerini genişleterek yukarıdaki dizede oldukça çirkin olanlardan kurtulabilirsiniz lines. $'...'Alıntı biçimi var kullanılabilir:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"

20
<<< "$(echo -e "$lines")"basit daha iyi bir değişiklik<<< "$lines"
beliy

ya kaynak tail -fsabit metin yerine ise?
mt eee

2
@mteee Kullanabilirsiniz while read -r line; do echo "LINE: $line"; done < <(tail -f file)(açıkçası döngü, girişi beklemeye devam ettikçe sonlanmaz tail).
PP

/ Bin / sh ile sınırlıyım - eski kabukta çalışan alternatif bir yol var mı?
user9645

1
@AvinashYadav Sorun gerçekten whiledöngü veya fordöngü ile ilgili değil ; yerine altkabuk yani kullanımı halinde cmd1 | cmd2, cmd2bir kabuktaki olduğunu. Dolayısıyla for, bir alt kabukta bir döngü yürütülürse, beklenmeyen / sorunlu davranış sergilenecektir.
PP

48

GÜNCELLENDİ 2.

Açıklama Blue Moons'un cevabında.

Alternatif çözümler:

Elemek echo

while read line; do
...
done <<EOT
first line
second line
third line
EOT

Yankıyı belgenin içine yerleştirin

while read line; do
...
done <<EOT
$(echo -e $lines)
EOT

echoArka planda çalıştır :

coproc echo -e $lines
while read -u ${COPROC[0]} line; do 
...
done

Açıkça bir dosya tanıtıcısına yönlendirin (İçindeki boşluğa dikkat edin < <!):

exec 3< <(echo -e  $lines)
while read -u 3 line; do
...
done

Veya şu adrese yönlendirin stdin:

while read line; do
...
done < <(echo -e  $lines)

Ve biri için chepner(ortadan kaldırmak echo):

arr=("first line" "second line" "third line");
for((i=0;i<${#arr[*]};++i)) { line=${arr[i]}; 
...
}

Değişken $lines, yeni bir alt kabuk başlatılmadan bir diziye dönüştürülebilir. Karakterler \ve nbazı karakterlere dönüştürülmelidir (örneğin, gerçek yeni bir satır karakteri) ve dizeyi dizi öğelerine bölmek için IFS (Dahili Alan Ayırıcı) değişkenini kullanın. Bu şu şekilde yapılabilir:

lines="first line\nsecond line\nthird line"
echo "$lines"
OIFS="$IFS"
IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion
IFS="$OIFS"
echo "${arr[@]}", Length: ${#arr[*]}
set|grep ^arr

Sonuç:

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")

Burada-doc için +1, çünkü linesdeğişkenin tek amacı whiledöngüyü beslemek gibi görünüyor .
chepner

@ chepner: Teşekkürler! Size adanmış bir tane daha ekledim!
TrueY

Burada verilen başka bir çözüm daha var :for line in $(echo -e $lines); do ... done
dma_k

@dma_k Yorumunuz için teşekkürler! Bu çözüm, tek bir sözcük içeren 6 satırla sonuçlanacaktır. OP'nin isteği farklıydı ...
True

upvoted. burada bir alt kabukta yankı çalışan, kül içinde çalışan birkaç çözümden biriydi
Hamy

9

Bu bash SSS'sini soracak 742342. kullanıcısın . Cevap ayrıca, borular tarafından oluşturulan alt kabuklarda ayarlanan değişkenlerin genel durumunu da açıklar:

E4) Bir komutun çıkışını içeri aktarırsam , okuma komutu bittiğinde read variableçıkış neden görünmüyor $variable?

Bu, Unix süreçleri arasındaki ebeveyn-çocuk ilişkisi ile ilgilidir. Sadece basit çağrıları değil, boru hatlarında çalışan tüm komutları etkiler read. Örneğin, bir komutun çıktısını whiletekrar tekrar çağıran bir döngüye bağlamak readaynı davranışa neden olur.

Bir boru hattının her elemanı, hatta bir yerleşik veya kabuk fonksiyonu bile, boru hattını çalıştıran kabuğun alt öğesi olan ayrı bir işlemde çalışır. Bir alt işlem, üst ortamını etkileyemez. Ne zaman readkomut girişine değişkeni ayarlar, bu değişken yalnızca alt kabuğa ekleyin, üst kabuk yer almaktadır. Alt kabuk çıktığında, değişkenin değeri kaybolur.

İle biten birçok boru hattı, read variablebelirtilen bir komutun çıktısını yakalayacak komut ikamelerine dönüştürülebilir. Çıkış daha sonra bir değişkene atanabilir:

grep ^gnu /usr/lib/news/active | wc -l | read ngroup

dönüştürülebilir

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)

Bu, ne yazık ki, birden çok değişken argümanı verildiğinde okunduğu gibi metni birden çok değişken arasında bölmeye çalışmaz. Bunu yapmanız gerekirse, çıktıyı bir değişkene okumak için yukarıdaki komut ikamesini kullanabilir ve bash desen kaldırma genişletme operatörlerini kullanarak değişkeni parçalayabilir veya aşağıdaki yaklaşımın bazı varyantlarını kullanabilirsiniz.

Say / usr / local / bin / ipaddr aşağıdaki kabuk betiğidir:

#! /bin/sh
host `hostname` | awk '/address/ {print $NF}'

Kullanmak yerine

/usr/local/bin/ipaddr | read A B C D

yerel makinenin IP adresini ayrı sekizlilere bölmek için

OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="$1" B="$2" C="$3" D="$4"

Ancak bunun kabuğun konum parametrelerini değiştireceğine dikkat edin. Onlara ihtiyacınız varsa, bunu yapmadan önce onları kaydetmelisiniz.

Bu genel yaklaşımdır - çoğu durumda $ IFS'yi farklı bir değere ayarlamanız gerekmez.

Kullanıcı tarafından sağlanan diğer alternatifler şunlardır:

read A B C D << HERE
    $(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE

ve süreç ikamesinin mevcut olduğu yerlerde,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))

7
Family Feud sorununu unuttun . Bazen yanıtı kim yazdıysa aynı kelime kombinasyonunu bulmak çok zordur, böylece ne yanlış sonuçlara boğulursunuz ne de söz konusu cevabı filtrelemezsiniz.
Evi1M4chine

3

Hmmm ... Bunun orijinal Bourne kabuğu için işe yaradığına yemin ederim, ancak kontrol etmek için şimdi çalışan bir kopyaya erişemiyorum.

Bununla birlikte, sorun için çok önemsiz bir çözüm vardır.

Komut dosyasının ilk satırını şununla değiştirin:

#!/bin/bash

için

#!/bin/ksh

Et voila! Bir boru hattının sonundaki bir okuma, Korn kabuğunun kurulu olduğu varsayılarak iyi çalışır.


1

Bir döngü içinde saklamak için stderr kullanın ve dışarıdan okuyun. Burada var i başlangıçta döngü içinde 1 olarak ayarlanır ve okunur.

# reading lines of content from 2 files concatenated
# inside loop: write value of var i to stderr (before iteration)
# outside: read var i from stderr, has last iterative value

f=/tmp/file1
g=/tmp/file2
i=1
cat $f $g | \
while read -r s;
do
  echo $s > /dev/null;  # some work
  echo $i > 2
  let i++
done;
read -r i < 2
echo $i

Veya bir alt kabuktaki kod miktarını azaltmak için heredoc yöntemini kullanın. Yinelemeli i değerinin while döngüsü dışında okunabileceğini unutmayın.

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i

0

Çok basit bir yönteme ne dersiniz?

    +call your while loop in a function 
     - set your value inside (nonsense, but shows the example)
     - return your value inside 
    +capture your value outside
    +set outside
    +display outside


    #!/bin/bash
    # set -e
    # set -u
    # No idea why you need this, not using here

    foo=0
    bar="hello"

    if [[ "$bar" == "hello" ]]
    then
        foo=1
        echo "Setting  \$foo to $foo"
    fi

    echo "Variable \$foo after if statement: $foo"

    lines="first line\nsecond line\nthird line"

    function my_while_loop
    {

    echo -e $lines | while read line
    do
        if [[ "$line" == "second line" ]]
        then
        foo=2; return 2;
        echo "Variable \$foo updated to $foo inside if inside while loop"
        fi

        echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2;          
    echo "Variable \$foo updated to $foo inside if inside while loop"
    return 2;
    fi

    # Code below won't be executed since we returned from function in 'if' statement
    # We aready reported the $foo var beint set to 2 anyway
    echo "Value of \$foo in while loop body: $foo"

done
}

    my_while_loop; foo="$?"

    echo "Variable \$foo after while loop: $foo"


    Output:
    Setting  $foo 1
    Variable $foo after if statement: 1
    Value of $foo in while loop body: 1
    Variable $foo after while loop: 2

    bash --version

    GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
    Copyright (C) 2007 Free Software Foundation, Inc.

6
Belki de burada yüzeyin altında iyi bir cevap var, ama biçimlendirmeyi denemek ve okumak hoş olmayan bir noktaya getirdiniz.
Mark Amery

Orijinal kodun okunması hoş mudur yani? (Az önce takip ettim: p)
Marcin

0

Bu ilginç bir soru ve Bourne kabuğu ve alt kabuğunda çok temel bir konsepte değiniyor. Burada bir tür filtreleme yaparak önceki çözümlerden farklı bir çözüm sağlıyorum. Gerçek hayatta faydalı olabilecek bir örnek vereceğim. Bu, indirilen dosyaların bilinen bir sağlama toplamına uygun olup olmadığını kontrol etmek için bir parçadır. Sağlama toplamı dosyası aşağıdaki gibi görünür (Yalnızca 3 satır gösteriliyor):

49174 36326 dna_align_feature.txt.gz
54757     1 dna.txt.gz
55409  9971 exon_transcript.txt.gz

Kabuk betiği:

#!/bin/sh

.....

failcnt=0 # this variable is only valid in the parent shell
#variable xx captures all the outputs from the while loop
xx=$(cat ${checkfile} | while read -r line; do
    num1=$(echo $line | awk '{print $1}')
    num2=$(echo $line | awk '{print $2}')
    fname=$(echo $line | awk '{print $3}')
    if [ -f "$fname" ]; then
        res=$(sum $fname)
        filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
        if [ "$filegood" = "FALSE" ]; then
            failcnt=$(expr $failcnt + 1) # only in subshell
            echo "$fname BAD $failcnt"
        fi
    fi
done | tail -1) # I am only interested in the final result
# you can capture a whole bunch of texts and do further filtering
failcnt=${xx#* BAD } # I am only interested in the number
# this variable is in the parent shell
echo failcnt $failcnt
if [ $failcnt -gt 0 ]; then
    echo $failcnt files failed
else
    echo download successful
fi

Üst öğe ve alt kabuk, echo komutu aracılığıyla iletişim kurar. Üst kabuk için ayrıştırılması kolay bazı metinler seçebilirsiniz. Bu yöntem, normal düşünme şeklinizi bozmaz, sadece bazı post işleme yapmak zorundasınız. Bunu yapmak için grep, sed, awk ve daha fazlasını kullanabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.