Bash: Wie können Fehler bei der Prozessersetzung weitergegeben werden?

19

Ich möchte, dass meine Shell-Skripte immer dann fehlschlagen, wenn ein mit ihnen ausgeführter Befehl fehlschlägt.

Normalerweise mache ich das mit:

set -e
set -o pipefail

(normalerweise füge ich set -uauch hinzu)

Die Sache ist, dass keines der oben genannten Verfahren mit Prozessersetzung funktioniert. Dieser Code gibt "ok" aus und beendet mit Returncode = 0, während ich möchte, dass es fehlschlägt:

#!/bin/bash -e
set -o pipefail
cat <(false) <(echo ok)

Gibt es etwas, das "Pipefail" entspricht, außer der Prozessersetzung? Gibt es eine andere Möglichkeit, die Ausgabe von Befehlen als Dateien an einen Befehl zu übergeben, aber einen Fehler auszulösen, wenn eines dieser Programme fehlschlägt?

Die Lösung eines armen Mannes wäre zu erkennen, ob diese Befehle in stderr schreiben (aber einige Befehle schreiben in erfolgreichen Szenarien in stderr).

Eine andere Lösung, die mehr mit Posix kompatibel ist, wäre die Verwendung von Named Pipes, aber ich muss diese Befehle, die Prozessersetzung verwenden, starten, da Oneliners aus kompiliertem Code automatisch erstellt werden. Das Erstellen von Named Pipes würde die Dinge komplizieren (zusätzliche Befehle, Überfüllungsfehler für Löschen usw.)

Juanleon
quelle
Sie müssen keine Fehler abfangen, um Named Pipes zu löschen. Tatsächlich ist das überhaupt kein guter Weg. Überlegen Sie mkfifo pipe; { rm pipe; cat <file; } >pipe. Dieser Befehl bleibt so lange hängen, bis ein Reader geöffnet wird, pipeda dies die Shell ausführt. open()Sobald sich ein Reader pipeim fs-Link für pipebefindet rm, wird die catDatei in den Deskriptor der Shell für diese Pipe kopiert. Und überhaupt, wenn Sie einen Fehler aus einem Prozessunter propagieren wollen tun: <( ! : || kill -2 "$$")
mikeserv
Vielen Dank für den Hinweis zum Löschen von Named Pipes. Leider $$funktioniert die Ersetzung bei mir nicht, da diese Befehlsersetzung nicht erfolgt, da der Befehl, der die Prozessersetzung verwendet, in einer Befehlspipeline ausgeführt wird, die aus einem "Nicht-Shell" -Code (Python) stammt. Wahrscheinlich sollte ich einen Unterprozess in Python erstellen und programmgesteuert weiterleiten.
Juanleon
Also benutze kill -2 0.
mikeserv
Leider würde "kill -2 0" Python töten (signalisieren). Ich freue mich nicht darauf, Signal-Handler für die Ausführung von Geschäftslogik in einer Multithread-Anwendung zu
schreiben
Wenn Sie keine Signale verarbeiten möchten, warum versuchen Sie dann, sie zu empfangen? Wie auch immer, ich gehe davon aus, dass Named Pipes am Ende die einfachste Lösung sind, wenn es nur darum geht, ein eigenes Framework zu erstellen und einen maßgeschneiderten Dispatcher einzurichten. Überwinde diese Hürde und alles kommt zusammen.
mikeserv

Antworten:

6

Sie könnten dieses Problem nur damit umgehen, zum Beispiel:

cat <(false || kill $$) <(echo ok)
other_command

Die Subshell des Skripts ist SIGTERMd, bevor der zweite Befehl ausgeführt werden kann ( other_command). Der echo okBefehl wird "manchmal" ausgeführt: Das Problem ist, dass Prozessersetzungen asynchron sind. Es gibt keine Garantie, dass der kill $$Befehl vor oder nach dem echo okBefehl ausgeführt wird. Es ist eine Frage der Zeitplanung des Betriebssystems.

Betrachten Sie ein Bash-Skript wie folgt:

#!/bin/bash
set -e
set -o pipefail
cat <(echo pre) <(false || kill $$) <(echo post)
echo "you will never see this"

Die Ausgabe dieses Skripts kann sein:

$ ./script
Terminated
$ echo $?
143           # it's 128 + 15 (signal number of SIGTERM)

Oder:

$ ./script
Terminated
$ pre
post

$ echo $?
143

Sie können es versuchen und nach ein paar Versuchen sehen Sie die zwei verschiedenen Aufträge in der Ausgabe. In der ersten wurde das Skript beendet, bevor die beiden anderen echoBefehle in den Dateideskriptor schreiben konnten. Im zweiten Fall wurde der Befehl falseoder killwahrscheinlich nach den echoBefehlen geplant.

Genauer gesagt: Der Systemaufruf signal()des killDienstprogramms, das das SIGTERMSignal an den Shells-Prozess sendet, wurde später oder früher als die Echo- write()Systemaufrufe geplant (oder zugestellt).

Das Skript wird jedoch angehalten und der Beendigungscode ist nicht 0. Es sollte daher Ihr Problem lösen.

Eine andere Lösung ist natürlich die Verwendung von Named Pipes. Es hängt jedoch von Ihrem Skript ab, wie komplex es wäre, Named Pipes oder die oben beschriebene Problemumgehung zu implementieren.

Verweise:

Chaos
quelle
2

Fürs Protokoll, und selbst wenn die Antworten und Kommentare gut und hilfreich waren, habe ich etwas anderes implementiert (ich hatte einige Einschränkungen beim Empfang von Signalen im übergeordneten Prozess, die ich in der Frage nicht erwähnt habe).

Im Grunde habe ich so etwas beendet:

command <(subcomand 2>error_file && rm error_file) <(....) ...

Dann überprüfe ich die Fehlerdatei. Wenn es existiert, weiß ich, welcher Unterbefehl fehlgeschlagen ist (und der Inhalt der Fehlerdatei kann nützlich sein). Ausführlicher und hackiger, als ich es ursprünglich wollte, aber weniger umständlich als das Erstellen von Named Pipes in einem einzeiligen Bash-Befehl.

Juanleon
quelle
Was auch immer für Sie arbeitet, ist normalerweise der beste Weg. Vielen Dank, dass Sie zurückgekommen sind und mir geantwortet haben - Selfies sind meine Favoriten.
mikeserv
2

Dieses Beispiel zeigt, wie man es killzusammen mit verwendet trap.

#! /bin/bash
failure ()
{
  echo 'sub process failed' >&2
  exit 1
}
trap failure SIGUSR1
cat < <( false || kill -SIGUSR1 $$ )

Es killkann jedoch kein Rückkehrcode von Ihrem Unterprozess an den übergeordneten Prozess übergeben werden.

ceving
quelle
Ich mag diese Variation der akzeptierten Antwort
sehe
1

Ähnlich wie Sie / mit einer POSIX-Shell implementieren$PIPESTATUS$pipestatus würden , die dies nicht unterstützt , können Sie den Beendigungsstatus der Befehle abrufen, indem Sie sie über eine Pipe weitergeben:

unset -v false_status echo_status
{ code=$(
    exec 3>&1 >&4 4>&-
    cat 3>&- <(false 3>&-; echo >&3 "false_status=$?") \
             <(echo ok 3>&-; echo >&3 "echo_status=$?")
);} 4>&1
cat_status=$?
eval "$code"
printf '%s_code=%d\n' cat   "$cat_status" \
                      false "$false_status" \
                      echo  "$echo_status"

Welches gibt:

ok
cat_code=0
false_code=1
echo_code=0

Oder Sie können die pipefailProzessersetzung von Hand verwenden und implementieren, wie Sie es mit Shells tun würden, die dies nicht unterstützen:

set -o pipefail
{
  false <&5 5<&- | {
    echo OK <&5 5<&- | {
      cat /dev/fd/3 /dev/fd/4
    } 4<&0 <&5 5<&-
  } 3<&0
} 5<&0
Stéphane Chazelas
quelle