Eine in einer while-Schleife geänderte Variable wird nicht gespeichert

185

Wenn ich im folgenden Programm die Variable $fooin der ersten ifAnweisung auf den Wert 1 setze , funktioniert dies in dem Sinne, dass ihr Wert nach der if-Anweisung gespeichert wird. Wenn ich jedoch dieselbe Variable ifinnerhalb einer whileAnweisung auf den Wert 2 setze , wird sie nach der whileSchleife vergessen . Es verhält sich so, als würde ich eine Kopie der Variablen $fooin der whileSchleife verwenden und nur diese bestimmte Kopie ändern. Hier ist ein vollständiges Testprogramm:

#!/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
quelle
Das Shellcheck-Dienstprogramm fängt dies ab (siehe github.com/koalaman/shellcheck/wiki/SC2030 ). Durch Ausschneiden und Einfügen des obigen Codes in shellcheck.net wird dieses Feedback für Zeile 19 SC2030: Modification of foo is local (to subshell caused by pipeline).
ausgegeben

Antworten:

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

Die whileSchleife wird in einer Unterschale ausgeführt. Änderungen, die Sie an der Variablen vornehmen, sind nach dem Beenden der Subshell nicht mehr verfügbar.

Stattdessen können Sie eine here-Zeichenfolge verwenden , um die while-Schleife neu zu schreiben, damit sie sich im Haupt-Shell-Prozess befindet. wird nur echo -e $linesin einer Subshell ausgeführt:

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

Sie können das ziemlich Hässliche echoin der obigen Zeichenfolge loswerden, indem Sie die Backslash-Sequenzen sofort beim Zuweisen erweitern lines. Dort kann die $'...'Form des Zitierens verwendet werden:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"
PP
quelle
20
besser <<< "$(echo -e "$lines")"zu einfach wechseln<<< "$lines"
beliy
Was ist, wenn die Quelle tail -fanstelle von festem Text stammt?
mt eee
2
@mteee Sie können verwenden while read -r line; do echo "LINE: $line"; done < <(tail -f file)(offensichtlich wird die Schleife nicht beendet, da sie weiterhin auf die Eingabe von wartet tail).
PP
Ich bin auf / bin / sh beschränkt - gibt es eine alternative Möglichkeit, die in der alten Shell funktioniert?
user9645
1
@AvinashYadav Das Problem hängt nicht wirklich mit whileSchleife oder forSchleife zusammen. Vielmehr ist die Verwendung von Subshell, dh in cmd1 | cmd2, cmd2in einer Subshell. Wenn also eine forSchleife in einer Subshell ausgeführt wird, wird das unerwartete / problematische Verhalten angezeigt.
PP
47

AKTUALISIERT # 2

Die Erklärung findet sich in der Antwort von Blue Moons.

Alternativlösungen:

Beseitigen echo

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

Fügen Sie das Echo im Here-is-the-Document hinzu

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

Führen Sie echoim Hintergrund:

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

Umleiten zu einem Dateihandle explizit (Beachten Sie den Platz in < <!):

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

Oder leiten Sie einfach weiter zu stdin:

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

Und eine für chepner(eliminieren echo):

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

Eine Variable $lineskann in ein Array konvertiert werden, ohne eine neue Sub-Shell zu starten. Die Zeichen \und nmüssen in ein Zeichen konvertiert werden (z. B. ein echtes neues Zeilenzeichen) und die Variable IFS (Internal Field Separator) verwenden, um die Zeichenfolge in Array-Elemente aufzuteilen. Dies kann wie folgt erfolgen:

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

Ergebnis ist

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
TrueY
quelle
+1 für das here-doc, da der lineseinzige Zweck der Variablen darin zu bestehen scheint, die whileSchleife zu speisen .
Chepner
@chepner: Danke! Ich habe eine weitere hinzugefügt, die Dir gewidmet ist!
TrueY
Es gibt noch eine andere Lösung gegeben hier :for line in $(echo -e $lines); do ... done
dma_k
@dma_k Danke für deinen Kommentar! Diese Lösung würde 6 Zeilen ergeben, die ein einzelnes Wort enthalten. OPs Anfrage war anders ...
TrueY
upvoted. Echo in einer Unterschale hier drinnen laufen zu lassen, war eine der wenigen Lösungen, die in Asche funktionierten
Hamy
9

Sie sind der 742342. Benutzer, der diese Bash-FAQ fragt . Die Antwort beschreibt auch den allgemeinen Fall von Variablen, die in von Pipes erstellten Unterschalen festgelegt sind:

E4) Wenn ich die Ausgabe eines Befehls anpfeife read variable, warum wird die Ausgabe dann nicht in angezeigt?$variable wenn der Lesebefehl beendet ist?

Dies hat mit der Eltern-Kind-Beziehung zwischen Unix-Prozessen zu tun. Dies betrifft alle Befehle, die in Pipelines ausgeführt werden, nicht nur einfache Aufrufe von read. Beispiel: Leiten Sie die Ausgabe eines Befehls in eine whileSchleife, die wiederholt aufgerufen wirdread wird, führt dies zu demselben Verhalten.

Jedes Element einer Pipeline, auch eine integrierte oder Shell-Funktion, wird in einem separaten Prozess ausgeführt, einem untergeordneten Element der Shell, in der die Pipeline ausgeführt wird. Ein Unterprozess kann die Umgebung seines übergeordneten Elements nicht beeinflussen. Wenn derread Befehl die Variable auf die Eingabe setzt, wird diese Variable nur in der Subshell und nicht in der übergeordneten Shell festgelegt. Wenn die Unterschale beendet wird, geht der Wert der Variablen verloren.

Viele Pipelines, die mit enden, read variablekönnen in Befehlsersetzungen konvertiert werden, die die Ausgabe eines angegebenen Befehls erfassen. Der Ausgang kann dann einer Variablen zugeordnet werden:

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

kann umgewandelt werden in

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

Dies funktioniert leider nicht, um den Text auf mehrere Variablen aufzuteilen, wie dies beim Lesen der Fall ist, wenn mehrere Variablenargumente angegeben werden. Wenn Sie dies tun müssen, können Sie entweder die obige Befehlssubstitution verwenden, um die Ausgabe in eine Variable einzulesen und die Variable mithilfe der Erweiterungsoperatoren zum Entfernen von Bash-Mustern zu zerlegen, oder eine Variante des folgenden Ansatzes verwenden.

Angenommen, / usr / local / bin / ipaddr ist das folgende Shell-Skript:

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

Anstatt zu verwenden

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

Verwenden Sie, um die IP-Adresse des lokalen Computers in separate Oktette zu unterteilen

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

Beachten Sie jedoch, dass dadurch die Positionsparameter der Shell geändert werden. Wenn Sie sie benötigen, sollten Sie sie vorher speichern.

Dies ist der allgemeine Ansatz. In den meisten Fällen müssen Sie $ IFS nicht auf einen anderen Wert setzen.

Einige andere vom Benutzer bereitgestellte Alternativen umfassen:

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

und wenn eine Prozesssubstitution verfügbar ist,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
Jens
quelle
7
Sie haben das Problem der Familienfehde vergessen . Manchmal ist es sehr schwierig, die gleiche Wortkombination zu finden wie derjenige, der die Antwort geschrieben hat, sodass Sie weder von falschen Ergebnissen überflutet werden noch diese Antwort herausfiltern.
Evi1M4chine
3

Hmmm ... Ich würde fast schwören, dass dies für die ursprüngliche Bourne-Shell funktioniert hat, aber ich habe gerade keinen Zugriff auf eine laufende Kopie, um dies zu überprüfen.

Es gibt jedoch eine sehr triviale Problemumgehung für das Problem.

Ändern Sie die erste Zeile des Skripts von:

#!/bin/bash

zu

#!/bin/ksh

Et voila! Ein Lesevorgang am Ende einer Pipeline funktioniert einwandfrei, vorausgesetzt, Sie haben die Korn-Shell installiert.

Jonathan Quist
quelle
0

Wie wäre es mit einer sehr einfachen Methode

    +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
quelle
6
Vielleicht gibt es hier eine anständige Antwort unter der Oberfläche, aber Sie haben die Formatierung so weit abgeschlachtet, dass es unangenehm ist, zu versuchen, sie zu lesen.
Mark Amery
Sie meinen, der Originalcode ist angenehm zu lesen? (Ich bin gerade gefolgt: p)
Marcin
0

Dies ist eine interessante Frage und berührt ein sehr grundlegendes Konzept in Bourne Shell und Subshell. Hier biete ich eine Lösung an, die sich von den vorherigen Lösungen durch eine Art Filterung unterscheidet. Ich werde ein Beispiel geben, das im wirklichen Leben nützlich sein kann. Dies ist ein Fragment zum Überprüfen, ob heruntergeladene Dateien einer bekannten Prüfsumme entsprechen. Die Prüfsummendatei sieht folgendermaßen aus (zeigt nur 3 Zeilen):

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

Das Shell-Skript:

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

Das übergeordnete Element und die Unterschale kommunizieren über den Befehl echo. Sie können einfach zu analysierenden Text für die übergeordnete Shell auswählen. Diese Methode bricht nicht Ihre normale Denkweise, nur dass Sie eine Nachbearbeitung durchführen müssen. Sie können dazu grep, sed, awk und mehr verwenden.

Kemin Zhou
quelle