Warum Piping-Eingang zu “lesen” funktioniert nur, wenn in “während lesen …” Konstrukt eingegeben?

Ich habe versucht, Eingaben in Umgebungsvariablen aus der Programmausgabe wie folgt zu lesen:

echo first second | read AB ; echo $A-$B 

Und das Ergebnis ist:

 - 

Sowohl A als auch B sind immer leer. Ich habe gelesen, dass bash piped Befehle in der Subshell ausführt, und dass es im Grunde verhindert wird, dass eine Pipe-Eingabe gelesen wird. Folgendes jedoch:

 echo first second | while read AB ; do echo $A-$B ; done 

Scheint zu arbeiten, das Ergebnis ist:

 first-second 

Kann jemand bitte erklären, was die Logik hier ist? Ist es so, dass die Befehle innerhalb des Konstrukts whiledone in der gleichen Shell wie echo und nicht in einer Subshell ausgeführt werden?

Wie man eine Schleife gegen Stdin macht und Ergebnis in einer Variablen speichert

Unter bash (und anderer Shell auch), wenn Sie etwas mit | Mit einem anderen Befehl erstellen Sie implizit eine Verzweigung, eine Subshell, die ein Kind der aktuellen Sitzung ist und die Umgebung der aktuellen Sitzung nicht beeinflussen kann.

Also das:

 TOTAL=0 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done echo final total: $TOTAL 

gibt kein erwartetes Ergebnis! :

  9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 echo final total: $TOTAL final total: 0 

Wo berechnete TOTAL nicht im Hauptskript wiederverwendet werden könnte.

Invertieren der Gabel

Mit bash Process Substitution , Here Documents oder Here Strings können Sie die Verzweigung invertieren:

Hier Streicher

 read AB < <<"first second" echo $A first echo $B second 

Hier Dokumente

 while read AB;do echo $A-$B C=$A-$B done < < eodoc first second third fourth eodoc first-second third-fourth 

außerhalb der Schleife:

 echo : $C : third-fourth 

Hier Befehle

 TOTAL=0 while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done < <( printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 ) 9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 # and finally out of loop: echo $TOTAL -343 

Jetzt könnten Sie $TOTAL in Ihrem Hauptskript verwenden .

Piping zu einer Befehlsliste

Aber um nur gegen Stdin zu arbeiten , können Sie eine Art Skript in die Verzweigung einfügen :

 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | { TOTAL=0 while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done echo "Out of the loop total:" $TOTAL } 

Wird geben:

  9 - 4 = 5 -> TOTAL= 5 3 - 1 = 2 -> TOTAL= 7 77 - 2 = 75 -> TOTAL= 82 25 - 12 = 13 -> TOTAL= 95 226 - 664 = -438 -> TOTAL= -343 Out of the loop total: -343 

Hinweis: $TOTAL konnte nicht im Hauptskript verwendet werden (nach der letzten geschweiften Klammer } ).

Verwenden der lastpipe bash-Option

Wie @CharlesDuffy richtig darauf hingewiesen hat, gibt es eine Bash-Option, um dieses Verhalten zu ändern. Dafür müssen wir zunächst die Jobsteuerung deaktivieren :

 shopt -s lastpipe # Set *lastpipe* option set +m # Disabling job control TOTAL=0 printf "%s %s\n" 9 4 3 1 77 2 25 12 226 664 | while read AB;do ((TOTAL+=AB)) printf "%3d - %3d = %4d -> TOTAL= %4d\n" $A $B $[AB] $TOTAL done 9 - 4 = 5 -> TOTAL= -338 3 - 1 = 2 -> TOTAL= -336 77 - 2 = 75 -> TOTAL= -261 25 - 12 = 13 -> TOTAL= -248 226 - 664 = -438 -> TOTAL= -686 echo final total: $TOTAL -343 

Das wird funktionieren, aber ich (persönlich) mag das nicht, weil das nicht Standard ist und nicht helfen wird, das Skript lesbar zu machen. Auch die Deaktivierung der Jobsteuerung scheint für den Zugriff auf dieses Verhalten teuer zu sein.

Hinweis: Die Jobsteuerung ist standardmäßig nur in interaktiven Sitzungen aktiviert. Daher ist set +m in normalen Skripten nicht erforderlich.

So vergessen set +m in einem Skript würde verschiedene Verhaltensweisen erstellen, wenn in einer Konsole ausgeführt oder wenn in einem Skript ausgeführt. Das macht das nicht einfach zu verstehen oder zu debuggen ...

Zuerst wird diese Pipe-Chain ausgeführt:

 echo first second | read AB 

dann

 echo $A-$B 

Da der read AB in einer Untershell ausgeführt wird, gehen A und B verloren. Wenn du das tust:

 echo first second | (read AB ; echo $A-$B) 

dann werden sowohl read AB als auch echo $A-$B in derselben Subshell ausgeführt (siehe manpage von bash, suche nach (list)

eine viel sauberere Work-around …

 read -rab < <(echo "$first $second") echo "$a $b" 

Auf diese Weise wird Lesen nicht in einer Subshell ausgeführt (was die Variablen löscht, sobald diese Subshell beendet ist). Stattdessen werden die Variablen, die Sie verwenden möchten, in einer Subshell zurückgegeben, die die Variablen automatisch von der übergeordneten Shell erbt.

Was Sie sehen, ist die Trennung zwischen processen: Das read geschieht in einer Subshell – ein separater process, der die Variablen im Hauptprozess nicht verändern kann (wo echo Befehle später auftreten).

Eine Pipeline (wie A | B ) platziert implizit jede Komponente in eine Subshell (einen separaten process), selbst für eingebaute (wie read ), die normalerweise im Kontext der Shell laufen (im selben process).

Der Unterschied im Fall von “into while” ist eine Illusion. Es gilt die gleiche Regel: Die Schleife ist die zweite Hälfte einer Pipeline, also befindet sie sich in einer Subshell, aber die gesamte Schleife befindet sich in derselben Subshell, so dass die Trennung von processen nicht gilt.