Warum wird eine Bash-while-Schleife nicht beendet, wenn eine Weiterleitung an einen terminierten Unterbefehl erfolgt?

12

Warum wird der folgende Befehl nicht beendet? Anstatt zu beenden, läuft die Schleife unbegrenzt.

Während ich dieses Verhalten mithilfe eines komplexeren Setups entdeckte, reduziert sich die einfachste Form des Befehls auf Folgendes.

Wird nicht beendet:

while /usr/bin/true ; do echo "ok" | cat ; done | exit 1

Es gibt oben keine Tippfehler. Jedes '|' ist eine Pfeife. Der 'Exit 1' steht für einen anderen Prozess, der ausgeführt und beendet wurde.

Ich erwarte, dass der "Ausgang 1" ein SIGPIPE in der while-Schleife verursacht (auf eine Pipe ohne Lesegerät schreiben) und dass die Schleife ausbricht. Die Schleife läuft jedoch weiter.

Warum stoppt der Befehl nicht?

stephen.z
quelle
zsh wird normal beendet.
Braiam

Antworten:

13

Es liegt an einer Wahl bei der Implementierung.

Das Ausführen des gleichen Skripts unter Solaris mit ksh93führt zu einem anderen Verhalten:

$ while /usr/bin/true ; do echo "ok" | cat ; done | exit 1
cat: write error [Broken pipe]

Was das Problem auslöst, ist die innere Pipeline. Ohne sie wird die Schleife unabhängig von der Shell / dem Betriebssystem beendet:

$ while /usr/bin/true ; do echo "ok" ; done | exit 1
$

cat erhält ein SIGPIPE-Signal unter bash, aber die Shell iteriert die Schleife trotzdem.

Process 5659 suspended
[pid 28801] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28801] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28801 detached
Process 28800 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28802 attached
Process 28803 attached
[pid 28803] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
Process 5659 suspended
[pid 28803] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28803 detached
Process 28802 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28804 attached
Process 28805 attached (waiting for parent)
Process 28805 resumed (parent 5659 ready)
Process 5659 suspended
[pid 28805] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28805] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28805 detached
Process 28804 detached
--- SIGCHLD (Child exited) @ 0 (0) ---

In der Bash- Dokumentation heißt es:

Die Shell wartet, bis alle Befehle in der Pipeline beendet sind, bevor ein Wert zurückgegeben wird.

In der Ksh- Dokumentation heißt es:

Jeder Befehl, außer möglicherweise der letzte, wird als separater Prozess ausgeführt. Die Shell wartet, bis der letzte Befehl beendet ist.

POSIX gibt an:

Befindet sich die Pipeline nicht im Hintergrund (siehe Asynchrone Listen), wartet die Shell auf den Abschluss des zuletzt in der Pipeline angegebenen Befehls und möglicherweise auch auf den Abschluss aller Befehle .

jlliagre
quelle
Ich denke, es ist nicht genau die innere Pipeline, die das Problem auslöst, sondern das eingebaute echoignoriert SIGPIPE. Sie können das Problem auch reproduzieren, indem Sie env echoanstelle von verwenden echo(um die Verwendung der eigentlichen echoBinärdatei zu erzwingen ). (Vergleiche auch die Ausgabe von { echo hi; echo $? >&2; } | exit 1und { env echo hi; echo $? >&2; } | exit 1.)
Lucas Werkmeister
1

Dieses Problem nervt mich seit Jahren. Vielen Dank an Jilliagre für den Anstoß in die richtige Richtung.

Wenn ich die Frage auf meiner Linux-Box ein wenig wiederhole, wird dies wie erwartet beendet:

while true ; do echo "ok"; done | head

Wenn ich jedoch eine Pipe hinzufüge, wird diese nicht wie erwartet beendet:

while true ; do echo "ok" | cat; done | head

Das hat mich jahrelang frustriert. Unter Berücksichtigung der Antwort von Jilliagre kam ich zu diesem wunderbaren Fix:

while true ; do echo "ok" | cat || exit; done | head

QED ...

Nicht ganz. Hier ist etwas etwas komplizierter:

i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' || exit
done | head

Das funktioniert nicht richtig. Ich habe das hinzugefügt, || exitdamit es weiß, wie man vorzeitig beendet, aber das allererste echostimmt nicht mit dem überein, grepsodass die Schleife sofort beendet wird. In diesem Fall interessiert Sie der Exit-Status des nicht wirklich grep. Meine Problemumgehung besteht darin, eine weitere hinzuzufügen cat. Also, hier ist ein erfundenes Skript namens "Zehner":

#!/bin/bash
i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' | cat || exit
done

Dies wird ordnungsgemäß beendet, wenn es als ausgeführt wird tens | head. Danke Gott.

PaulC
quelle