Warum befindet sich meine Variable local in einer 'while read'-Schleife, aber nicht in einer anderen scheinbar ähnlichen Schleife?

25

Warum erhalte ich $xaus den folgenden Ausschnitten andere Werte ?

#!/bin/bash

x=1
echo fred > junk ; while read var ; do x=55 ; done < junk
echo x=$x 
#    x=55 .. I'd expect this result

x=1
cat junk | while read var ; do x=55 ; done
echo x=$x 
#    x=1 .. but why?

x=1
echo fred | while read var ; do x=55 ; done
echo x=$x 
#    x=1  .. but why?
Peter.O
quelle
Ein ähnlicher Beitrag zum Stapelüberlauf: Eine in einer while-Schleife geänderte Variable wird nicht gespeichert .
Codeforester

Antworten:

26

Die richtige Erklärung wurde bereits von jsbillings und geekosaur gegeben , aber lassen Sie mich das etwas näher erläutern .

In den meisten Shells, einschließlich Bash, wird jede Seite einer Pipeline in einer Subshell ausgeführt, sodass jede Änderung des internen Status der Shell (z. B. das Festlegen von Variablen) auf dieses Segment einer Pipeline beschränkt bleibt. Die einzige Information, die Sie von einer Subshell erhalten können, ist die Ausgabe (für die Standardausgabe und andere Dateideskriptoren) und der Exit-Code (eine Zahl zwischen 0 und 255). Das folgende Snippet gibt beispielsweise 0 aus:

a=0; a=1 | a=2; echo $a

In ksh (die vom AT & T-Code abgeleiteten Varianten, nicht pdksh / mksh-Varianten) und zsh wird das letzte Element in einer Pipeline in der übergeordneten Shell ausgeführt. (POSIX erlaubt beide Verhaltensweisen.) Das obige Snippet gibt also 2 aus.

Eine nützliche Redewendung ist, die Fortsetzung der while-Schleife (oder was auch immer Sie auf der rechten Seite der Pipeline haben, aber eine while-Schleife ist hier tatsächlich üblich) in die Pipeline aufzunehmen:

cat junk | {
  while read var ; do x=55 ; done
  echo x=$x 
}
Gilles 'SO - hör auf böse zu sein'
quelle
1
Danke Gilles. Das a = 0; a = 1 | a = 2 gibt ein sehr klares Bild .. und nicht nur die Lokalisierung des internen Zustands, sondern auch, dass eine Pipeline eigentlich nichts durch die Pipe senden muss (außer dem Exit-Code (?) .. An sich schon ist ein interessanter Einblick in eine Pipe ... Ich habe es geschafft, mein Skript zum Laufen zu bringen < <(locate -ber ^\.tag$), dank der ursprünglichen, leicht unklaren Antwort und der Kommentare von Geekosaurier und Glenn Jackman. Ich war anfangs in einem Dilemma, die Antwort zu akzeptieren, aber das Nettoergebnis war ziemlich klar, vor allem mit jsbillings Follow-up-Kommentar :)
Peter.O
Es fühlt sich an, als wäre ich in eine Funktion eingebunden, also habe ich einige Variablen und Tests in sie verschoben und es hat großartig funktioniert, danke!
Aquarius Power
8

Sie stoßen auf ein Problem mit dem variablen Bereich. Die in der while-Schleife auf der rechten Seite der Pipe definierten Variablen haben einen eigenen lokalen Gültigkeitsbereichskontext. Änderungen an der Variablen werden außerhalb der Schleife nicht angezeigt. Die while-Schleife ist im Wesentlichen eine Subshell, die ein COPY der Shell-Umgebung abruft , und alle Änderungen an der Umgebung gehen am Ende der Shell verloren. Siehe diese StackOverflow-Frage .

AKTUALISIERT : Ich habe versäumt, auf die wichtige Tatsache hinzuweisen, dass die while-Schleife mit ihrer eigenen Subshell der Endpunkt einer Pipe ist. Ich habe dies in der Antwort aktualisiert.

jsbillings
quelle
@jsbillings .. Okay, das erklärt die beiden letzten Schnipsel, aber nicht das erste, bei dem der in der Schleife festgelegte Wert von $ x als 55 weitergeführt wird (außerhalb des Bereichs der 'while'-Schleife)
Peter.O
5
@ fred.bear: Die whileSchleife wird als hinteres Ende einer Pipeline ausgeführt, die sie in eine Subshell wirft.
Geekosaurier
2
Hier kommt die Bash-Prozess-Substitution ins Spiel. Stattdessen blah|blah|while read ...können Sie habenwhile read ...; done < <(blah|blah)
Glenn Jackman
1
@geekosaur: danke, dass du die Details ausgefüllt hast, die ich in meiner Antwort nicht berücksichtigt habe.
Jsbillings
1
-1 Sorry, aber diese Antwort ist einfach falsch. Es wird erklärt, wie dieses Zeug in vielen Programmiersprachen funktioniert, jedoch nicht in der Shell. @ Gilles, unten, hat es richtig gemacht.
JPC
6

Wie in anderen Antworten erwähnt , verlaufen die Teile einer Pipeline in Unterschalen, sodass dort vorgenommene Änderungen für die Hauptschale nicht sichtbar sind.

Wenn wir nur Bash betrachten, gibt es neben der cmd | { stuff; more stuff; }Struktur noch zwei weitere Problemumgehungen :

  1. Leiten Sie die Eingabe von der Prozessersetzung um :

    while read var ; do x=55 ; done < <(echo fred)
    echo "$x"

    Die Ausgabe des Befehls in <(...)sieht aus, als wäre es eine Named Pipe.

  2. Die lastpipeOption, mit der Bash wie ksh funktioniert und den letzten Teil der Pipeline im Hauptshellprozess ausführt. Dies funktioniert jedoch nur, wenn die Auftragssteuerung deaktiviert ist, dh nicht in einer interaktiven Shell:

    bash -c '
      shopt -s lastpipe
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

    oder

    bash -O lastpipe -c '
      echo fred | while read var ; do x=55 ; done; 
      echo "$x"
    '

Die Prozessersetzung wird natürlich auch in ksh und zsh unterstützt. Da der letzte Teil der Pipeline ohnehin in der Hauptshell ausgeführt wird, ist es nicht unbedingt erforderlich, ihn als Workaround zu verwenden.

ilkkachu
quelle
0
#!/bin/bash
set -x

# prepare test data.
mkdir -p ~/test_var_global
cd ~/test_var_global
echo "a"> core.1
echo "b"> core.2
echo "c"> core.3


var=0

coreFiles=$(find . -type f -name "core*")
while read -r file;
do
  # perform computations on $i
  ((var++))
done <<EOF
$coreFiles
EOF

echo $var

Result:
...
+ echo 3
3

es kann funktionieren.

GPS
quelle