Fehlercodes in einem Shell-Rohr abfangen

97

Ich habe derzeit ein Skript, das so etwas tut

./a | ./b | ./c

Ich möchte es so ändern, dass ich, wenn eines von a, b oder c mit einem Fehlercode beendet wird, eine Fehlermeldung drucke und stoppe, anstatt eine schlechte Ausgabe weiterzuleiten.

Was wäre der einfachste / sauberste Weg, dies zu tun?

Hugomg
quelle
7
Es muss wirklich so etwas geben, &&|was bedeuten würde "die Pipe nur fortsetzen, wenn der vorhergehende Befehl erfolgreich war". Ich nehme an, Sie könnten auch haben, |||was bedeuten würde "die Pipe fortsetzen, wenn der vorhergehende Befehl fehlgeschlagen ist" (und möglicherweise die Fehlermeldung wie bei Bash 4 weiterleiten |&).
Bis auf weiteres angehalten.
6
@DennisWilliamson, können Sie nicht „das Rohr stoppen“ , weil a, b, cBefehle nacheinander nicht ausgeführt werden , sondern parallel. Mit anderen Worten, fließen die Daten sequentiell von azu c, aber die eigentlichen a, bund cBefehlen starten (grob) zur gleichen Zeit.
Giacomo

Antworten:

20

Wenn Sie wirklich nicht möchten, dass der zweite Befehl fortgesetzt wird, bis bekannt ist, dass der erste erfolgreich ist, müssen Sie wahrscheinlich temporäre Dateien verwenden. Die einfache Version davon ist:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Die Umleitung '1> & 2' kann auch mit '> & 2' abgekürzt werden. Eine alte Version der MKS-Shell hat die Fehlerumleitung jedoch ohne die vorhergehende '1' falsch behandelt, sodass ich diese eindeutige Notation für die Zuverlässigkeit seit Ewigkeiten verwendet habe.

Dadurch werden Dateien verloren, wenn Sie etwas unterbrechen. Bombensichere (mehr oder weniger) Shell-Programmierung verwendet:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

In der ersten Trap-Zeile steht "Führen Sie die Befehle aus" rm -f $tmp.[12]; exit 1, wenn eines der Signale 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE oder 15 SIGTERM auftritt, oder 0 (wenn die Shell aus irgendeinem Grund beendet wird). Wenn Sie ein Shell-Skript schreiben, muss der letzte Trap nur den Trap auf 0 entfernen, bei dem es sich um den Shell-Exit-Trap handelt (Sie können die anderen Signale an Ort und Stelle belassen, da der Prozess ohnehin kurz vor dem Abschluss steht).

In der ursprünglichen Pipeline ist es möglich, dass 'c' Daten von 'b' liest, bevor 'a' beendet ist - dies ist normalerweise wünschenswert (es gibt zum Beispiel mehrere Kerne Arbeit zu erledigen). Wenn 'b' eine 'Sortierphase' ist, gilt dies nicht - 'b' muss alle Eingaben sehen, bevor es eine Ausgabe erzeugen kann.

Wenn Sie feststellen möchten, welche Befehle fehlschlagen, können Sie Folgendes verwenden:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Dies ist einfach und symmetrisch - es ist trivial, sich auf eine 4-teilige oder N-teilige Pipeline zu erstrecken.

Einfaches Experimentieren mit 'set -e' hat nicht geholfen.

Jonathan Leffler
quelle
1
Ich würde empfehlen, mktempoder zu verwenden tempfile.
Bis auf weiteres angehalten.
@ Tennis: Ja, ich denke, ich sollte mich an Befehle wie mktemp oder tmpfile gewöhnen. Sie existierten nicht auf Shell-Ebene, als ich es vor so vielen Jahren lernte. Lassen Sie uns einen kurzen Check machen. Ich finde mktemp unter MacOS X; Ich habe mktemp unter Solaris, aber nur, weil ich GNU-Tools installiert habe. es scheint, dass mktemp auf antikem HP-UX vorhanden ist. Ich bin nicht sicher, ob es einen gemeinsamen Aufruf von mktemp gibt, der plattformübergreifend funktioniert. POSIX standardisiert weder mktemp noch tmpfile. Ich habe tmpfile auf den Plattformen, auf die ich Zugriff habe, nicht gefunden. Folglich kann ich die Befehle nicht in tragbaren Shell-Skripten verwenden.
Jonathan Leffler
1
Beachten Sie bei der Verwendung trap, dass der Benutzer jederzeit SIGKILLan einen Prozess senden kann, um ihn sofort zu beenden. In diesem Fall wird Ihre Falle nicht wirksam. Gleiches gilt, wenn in Ihrem System ein Stromausfall auftritt. Stellen Sie beim Erstellen temporärer Dateien sicher, dass Sie diese verwenden, mktempda dadurch Ihre Dateien an einem Ort abgelegt werden, an dem sie nach einem Neustart bereinigt werden (normalerweise in /tmp).
Josch
155

In Bash können Sie set -eund set -o pipefailam Anfang Ihrer Datei verwenden. Ein nachfolgender Befehl ./a | ./b | ./cschlägt fehl, wenn eines der drei Skripte fehlschlägt. Der Rückkehrcode ist der Rückkehrcode des ersten fehlgeschlagenen Skripts.

Beachten Sie, dass pipefailin Standard sh nicht verfügbar ist .

Michel Samia
quelle
Ich wusste nichts über Pipefail, sehr praktisch.
Phil Jackson
Es soll in ein Skript eingefügt werden, nicht in die interaktive Shell. Ihr Verhalten kann vereinfacht werden, um -e festzulegen. falsch; es verlässt auch den Shell-Prozess, der erwartetes Verhalten ist;)
Michel Samia
11
Hinweis: Dadurch werden weiterhin alle drei Skripts ausgeführt und die Pipe wird beim ersten Fehler nicht gestoppt.
Hyde
1
@ n2liquid-GuilhermeVieira Btw, mit "verschiedenen Variationen", meinte ich speziell, entfernen Sie eine oder beide der set (für insgesamt 4 verschiedene Versionen) und sehen Sie, wie sich dies auf die Ausgabe der letzten auswirkt echo.
Hyde
1
@josch Ich habe diese Seite gefunden, als ich nach der Vorgehensweise in Bash von Google gesucht habe und obwohl "Dies ist genau das, wonach ich suche". Ich vermute, dass viele Leute, die Antworten positiv bewerten, einen ähnlichen Denkprozess durchlaufen und die Tags und Definitionen der Tags nicht überprüfen.
Troy Daniels
43

Sie können das ${PIPESTATUS[]}Array auch nach der vollständigen Ausführung überprüfen , z. B. wenn Sie Folgendes ausführen:

./a | ./b | ./c

Dann ${PIPESTATUS}wird ein Array von Fehlercodes von jedem Befehl in der Pipe angezeigt. Wenn der mittlere Befehl fehlschlägt, echo ${PIPESTATUS[@]}enthält er beispielsweise Folgendes:

0 1 0

und so etwas läuft nach dem Befehl:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

Hier können Sie überprüfen, ob alle Befehle in der Pipe erfolgreich waren.

Imron
quelle
11
Dies ist Bashish - es ist eine Bash-Erweiterung und nicht Teil des Posix-Standards, sodass andere Shells wie Dash und Ash dies nicht unterstützen. Dies bedeutet, dass Sie Probleme bekommen können, wenn Sie versuchen, es in Skripten zu verwenden, die gestartet #!/bin/shwerden. Wenn dies shnicht Bash ist, funktioniert es nicht. (Leicht zu reparieren, indem man daran denkt, #!/bin/bashstattdessen zu verwenden .)
David Given
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- gibt 0 zurück, wenn $PIPESTATUS[@]nur 0 und Leerzeichen enthalten sind (wenn alle Befehle in der Pipe erfolgreich sind).
MattBianco
@MattBianco Dies ist die beste Lösung für mich. Es funktioniert auch mit && zB command1 && command2 | command3Wenn einer dieser Fehler auftritt, gibt Ihre Lösung einen Wert ungleich Null zurück.
DavidC
8

Leider erfordert die Antwort von Johnathan temporäre Dateien und die Antworten von Michel und Imron erfordern Bash (obwohl diese Frage als Shell gekennzeichnet ist). Wie bereits von anderen erwähnt, ist es nicht möglich, die Pipe abzubrechen, bevor spätere Prozesse gestartet werden. Alle Prozesse werden auf einmal gestartet und somit ausgeführt, bevor Fehler gemeldet werden können. Der Titel der Frage bezog sich aber auch auf Fehlercodes. Diese können abgerufen und untersucht werden, nachdem das Rohr fertig ist, um herauszufinden, ob einer der beteiligten Prozesse fehlgeschlagen ist.

Hier ist eine Lösung, die alle Fehler in der Pipe und nicht nur die Fehler der letzten Komponente abfängt. Das ist also wie Bashs Pipefail, nur mächtiger in dem Sinne, dass Sie alle Fehlercodes abrufen können .

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Um festzustellen, ob etwas fehlgeschlagen ist, wird ein echoStandardfehler ausgegeben, falls ein Befehl fehlschlägt. Dann wird die kombinierte Standardfehlerausgabe gespeichert $resund später untersucht. Aus diesem Grund wird der Standardfehler aller Prozesse auch auf die Standardausgabe umgeleitet. Sie können diese Ausgabe auch an /dev/nulleinen anderen Indikator senden oder als weiteren Indikator dafür belassen, dass ein Fehler aufgetreten ist. Sie können die letzte Weiterleitung an ersetzen/dev/null durch eine Datei wenn Sie die Ausgabe des letzten Befehls an einer beliebigen Stelle speichern möchten.

So spielen mehr mit diesem Konstrukt und sich selbst davon überzeugen , dass dies wirklich tut , was es soll, ich ersetzt ./a, ./bund ./cdurch Subshells , die ausgeführt werden echo, catund exit. Sie können dies verwenden, um zu überprüfen, ob dieses Konstrukt wirklich die gesamte Ausgabe von einem Prozess an einen anderen weiterleitet und ob die Fehlercodes korrekt aufgezeichnet werden.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
josch
quelle