Zmienna zmodyfikowana w pętli while nie jest zapamiętywana

187

W poniższym programie, jeśli ustawię zmienną $foona wartość 1 w pierwszej ifinstrukcji, działa ona w tym sensie, że jej wartość jest zapamiętywana po instrukcji if. Jednak gdy ustawię tę samą zmienną na wartość 2 wewnątrz instrukcji, ifktóra jest wewnątrz whileinstrukcji, jest ona zapominana po whilepętli. Zachowuje się tak, jakbym używał jakiejś kopii zmiennej $foowewnątrz whilepętli i modyfikuję tylko tę konkretną kopię. Oto pełny program testowy:

#!/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)
Eric Lilja
źródło
Narzędzie shellcheck łapie to (patrz github.com/koalaman/shellcheck/wiki/SC2030 ); Wytnij i wklej powyższy kod w shellcheck.net wydaje tę informację zwrotną dla wiersza 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Odpowiedzi:

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

whilePętla wykonywana jest w podpowłoce. Tak więc wszelkie zmiany wprowadzone w zmiennej nie będą dostępne po wyjściu podpowłoki.

Zamiast tego możesz użyć ciągu tutaj, aby ponownie napisać pętlę while, aby być w głównym procesie powłoki; echo -e $linesbędzie działał tylko w podpowłoce:

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")"

Możesz pozbyć się raczej brzydkiego echoz powyższego ciągu, rozszerzając sekwencje odwrotnego ukośnika natychmiast po przypisaniu lines. $'...'Forma cytowania mogą być stosowane tam:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"
PP
źródło
20
lepiej zmienić <<< "$(echo -e "$lines")"na prosty<<< "$lines"
najprawdopodobniej
co jeśli źródło pochodziło tail -fzamiast stałego tekstu?
Mt eee
2
@mteee Możesz użyć while read -r line; do echo "LINE: $line"; done < <(tail -f file)(oczywiście pętla nie kończy się, ponieważ nadal czeka na dane wejściowe tail).
PP
Jestem ograniczony do / bin / sh - czy istnieje alternatywny sposób, który działa w starej powłoce?
user9645,
1
@AvinashYadav Problemem nie jest bardzo podobne do whilepętli lub forpętli; raczej użycie podpowłoki, tj. w cmd1 | cmd2, cmd2jest w podpowłoce. Więc jeśli forpętla jest wykonywana w podpowłoce, zostanie pokazane nieoczekiwane / problematyczne zachowanie.
PP
48

ZAKTUALIZOWANO # 2

Wyjaśnienie znajduje się w odpowiedzi Blue Moons.

Alternatywne rozwiązania:

Wyeliminować echo

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

Dodaj echo do dokumentu tutaj jest

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

Uruchom echow tle:

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

Przekieruj jawnie do uchwytu pliku (Uwaga na miejsce < <!):

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

Lub po prostu przekieruj do stdin :

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

I jeden za chepner (eliminowanie echo):

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

Zmienna $linesmoże zostać przekonwertowana na tablicę bez uruchamiania nowej podpowłoki. Znaki \i nnależy przekonwertować na jakiś znak (np. Prawdziwy nowy znak linii) i użyć zmiennej IFS (Internal Field Separator) do podzielenia łańcucha na elementy tablicy. Można to zrobić w następujący sposób:

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

Wynik jest

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
Prawda
źródło
+1 dla dokumentu tutaj, ponieważ lineswydaje się, że jedynym celem zmiennej jest zasilenie whilepętli.
chepner
@chepner: Thx! Dodałem kolejny, dedykowany Tobie!
Prawda
Jest jeszcze inne rozwiązanie podane tutaj :for line in $(echo -e $lines); do ... done
dma_k
@dma_k Dziękujemy za komentarz! To rozwiązanie dałoby 6 linii zawierających jedno słowo. Prośba OP była inna ...
TrueY
pozytywnie oceniany. uruchamianie echa w podpowłoce wewnątrz było - było jednym z niewielu rozwiązań, które działało w popiele
Hamy
9

Jesteś 742342. użytkownikiem, który zadaje to bash FAQ. Odpowiedź opisuje również ogólny przypadek zmiennych ustawionych w podpowłokach utworzonych przez potoki:

E4) Jeśli wpuszczę dane wyjściowe polecenia do potoku read variable, dlaczego dane wyjściowe nie są wyświetlane $variablepo zakończeniu polecenia odczytu?

Ma to związek z relacją rodzic-dziecko między procesami uniksowymi. Wpływa na wszystkie polecenia uruchamiane w potokach, a nie tylko na proste wywołania read. Na przykład, potokowanie wyniku polecenia w whilepętli, która wielokrotnie wywołuje, readspowoduje takie samo zachowanie.

Każdy element potoku, nawet funkcja wbudowana lub powłoka, działa w osobnym procesie, potomku powłoki uruchamiającej potok. Podproces nie może wpływać na środowisko jego rodzica. Gdy readpolecenie ustawia zmienną na wejście, zmienna ta jest ustawiana tylko w podpowłoce, a nie w powłoce nadrzędnej. Po wyjściu podpowłoki wartość zmiennej jest tracona.

Wiele potoków zakończonych read variableznakiem można przekształcić w podstawienia poleceń, które przechwycą dane wyjściowe określonego polecenia. Dane wyjściowe można następnie przypisać do zmiennej:

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

można przekształcić w

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

Niestety nie działa to na podzielenie tekstu na wiele zmiennych, tak jak w przypadku odczytu wielu argumentów zmiennych. Jeśli musisz to zrobić, możesz albo użyć powyższego podstawienia polecenia, aby odczytać dane wyjściowe do zmiennej i posiekać zmienną za pomocą operatorów rozwijania usuwania wzorca bash, lub użyć jakiegoś wariantu następującego podejścia.

Powiedzmy, że / usr / local / bin / ipaddr to następujący skrypt powłoki:

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

Zamiast używać

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

aby podzielić adres IP komputera lokalnego na osobne oktety, użyj

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

Uważaj jednak, że to zmieni parametry pozycyjne powłoki. Jeśli ich potrzebujesz, powinieneś je zapisać przed zrobieniem tego.

Jest to ogólne podejście - w większości przypadków nie trzeba ustawiać $ IFS na inną wartość.

Niektóre inne alternatywy dostarczone przez użytkownika obejmują:

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

a tam, gdzie możliwe jest zastąpienie procesu,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
Jens
źródło
7
Zapomniałeś problemu waśni rodzinnych . Czasami bardzo trudno jest znaleźć taką samą kombinację słów, jak ktokolwiek, kto napisał odpowiedź, aby nie zalać się złymi wynikami ani odfiltrować tej odpowiedzi.
Evi1M4chine
3

Hmmm ... prawie przysiągłbym, że to zadziałało dla oryginalnej powłoki Bourne'a, ale nie mam teraz dostępu do działającej kopii, aby to sprawdzić.

Istnieje jednak bardzo trywialne obejście tego problemu.

Zmień pierwszy wiersz skryptu z:

#!/bin/bash

do

#!/bin/ksh

Zrobione! Odczyt na końcu potoku działa dobrze, zakładając, że masz zainstalowaną powłokę Korna.

Jonathan Quist
źródło
0

A może bardzo prosta metoda

    +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.
Marcin
źródło
6
Być może pod powierzchnią znajduje się przyzwoita odpowiedź, ale tak sformatowałeś formatowanie, że nie jest przyjemnie czytać.
Mark Amery
Masz na myśli, że oryginalny kod jest przyjemny do odczytania? (Właśnie obserwowałem: p)
Marcin
0

To interesujące pytanie dotyczy bardzo podstawowej koncepcji powłoki i podpowłoki Bourne'a. Tutaj zapewniam rozwiązanie, które różni się od poprzednich rozwiązań, wykonując pewnego rodzaju filtrowanie. Podam przykład, który może być przydatny w prawdziwym życiu. Jest to fragment do sprawdzania, czy pobrane pliki są zgodne ze znaną sumą kontrolną. Plik sumy kontrolnej wygląda następująco (pokazuje tylko 3 linie):

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

Skrypt powłoki:

#!/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

Jednostka nadrzędna i podpowłoka komunikują się za pomocą polecenia echo. Możesz wybrać łatwy do przeanalizowania tekst dla powłoki nadrzędnej. Ta metoda nie łamie twojego normalnego sposobu myślenia, wystarczy, że będziesz musiał wykonać pewne przetwarzanie końcowe. Możesz do tego użyć grep, sed, awk i innych.

Kemin Zhou
źródło