Ermittelt den Exit-Status eines Prozesses, der an einen anderen weitergeleitet wird


Antworten:


256

Wenn Sie verwenden bash, können Sie die PIPESTATUSArray-Variable verwenden, um den Beendigungsstatus jedes Elements der Pipeline abzurufen.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Wenn Sie verwenden zsh, wird das Array aufgerufen pipestatus(Groß- und Kleinschreibung!) Und die Array-Indizes beginnen bei eins:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

So kombinieren Sie sie in einer Funktion auf eine Weise, bei der die Werte nicht verloren gehen:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Führen Sie den obigen Befehl in bashoder aus, zshund Sie erhalten die gleichen Ergebnisse. es wird nur eines von retval_bashund retval_zshgesetzt. Der andere wird leer sein. Dies würde es einer Funktion ermöglichen, mit zu enden return $retval_bash $retval_zsh(beachten Sie das Fehlen von Anführungszeichen!).


9
Und pipestatusin zsh. Leider haben andere Shells diese Funktion nicht.
Gilles

8
Hinweis: Arrays in zsh beginnen mit Index 1, also mit Index 1 echo "$pipestatus[1]" "$pipestatus[2]".
Christoph Wurm

5
Sie können die gesamte Pipeline folgendermaßen überprüfen:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

7
@JanHudec: Vielleicht solltest du die ersten fünf Wörter meiner Antwort lesen. Bitte weisen Sie auch darauf hin, wo für die Frage eine reine POSIX-Antwort angefordert wurde.
16.

12
@JanHudec: Es wurde auch nicht mit POSIX markiert. Warum muss die Antwort vermutlich POSIX sein? Es wurde nicht spezifiziert, also gab ich eine qualifizierte Antwort. Meine Antwort ist nicht falsch, und es gibt mehrere Antworten, um andere Fälle anzusprechen.
17.

238

Es gibt drei Möglichkeiten, dies zu tun:

Pipefail

Die erste Möglichkeit besteht darin, die pipefailOption ( ksh, zshoder bash) festzulegen . Dies ist die einfachste und setzt im Grunde den Beendigungsstatus $?auf den Beendigungscode des letzten Programms, um das Programm zu beenden, der nicht Null ist (oder Null, wenn alle erfolgreich beendet wurden).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash hat auch eine Array-Variable namens $PIPESTATUS( $pipestatusin zsh), die den Exit-Status aller Programme in der letzten Pipeline enthält.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

Sie können das dritte Befehlsbeispiel verwenden, um den bestimmten Wert in der Pipeline abzurufen, den Sie benötigen.

Getrennte Ausführungen

Dies ist die unhandlichste der Lösungen. Führen Sie jeden Befehl separat aus und erfassen Sie den Status

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

2
Verdammt! Ich wollte gerade über PIPESTATUS posten.
SLM

1
Als Referenz gibt es mehrere andere Techniken, die in dieser SO-Frage erörtert werden: stackoverflow.com/questions/1221833/…
slm

1
@Patrick Die Pipestatus-Lösung funktioniert auf Bash, nur mehr Quastion, falls ich ein ksh-Skript verwende. Glauben Sie, wir können etwas Ähnliches wie Pipestatus finden? , (während ich sehe, dass der Pipestatus nicht von ksh unterstützt wird)
Yael

2
@yael benutze ich nicht ksh, aber von einem kurzen Blick auf die Manpage her unterstützt es nichts $PIPESTATUSoder ähnliches. Die pipefailOption wird jedoch unterstützt.
Patrick

1
Ich habe mich für Pipefail entschieden, da ich hier den Status des fehlgeschlagenen Befehls LOG=$(failed_command | successful_command)
abrufen kann

51

Diese Lösung funktioniert ohne bash-spezifische Funktionen oder temporäre Dateien. Bonus: Am Ende ist der Beendigungsstatus tatsächlich ein Beendigungsstatus und keine Zeichenfolge in einer Datei.

Lage:

someprog | filter

Sie möchten den Exit-Status von someprogund die Ausgabe von filter.

Hier ist meine Lösung:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

Das Ergebnis dieses Konstrukts ist stdout von filterals stdout des Konstrukts und exit status von someprogals exit status des Konstrukts.


Dieses Konstrukt funktioniert auch mit einfacher Befehlsgruppierung {...}anstelle von Subshells (...). Subshells haben einige Auswirkungen, unter anderem einen Leistungsaufwand, den wir hier nicht benötigen. Weitere Informationen finden Sie im Handbuch zu Fine Bash: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Leider erfordert die Bash-Grammatik Leerzeichen und Semikolons für die geschweiften Klammern, damit das Konstrukt viel geräumiger wird.

Für den Rest dieses Textes werde ich die Subshell-Variante verwenden.


Beispiel someprogund filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Beispielausgabe:

filtered line1
filtered line2
filtered line3
42

Hinweis: Der untergeordnete Prozess erbt die geöffneten Dateideskriptoren vom übergeordneten Prozess. Das bedeutet, someprogdass die offenen Dateideskriptoren 3 und 4 übernommen werden. Wenn someprogin Dateideskriptor 3 geschrieben wird, wird dies zum Beendigungsstatus. Der tatsächliche Beendigungsstatus wird ignoriert, da readnur einmal gelesen wird.

Wenn Sie befürchten, dass someprogin den Dateideskriptor 3 oder 4 geschrieben wird, sollten Sie die Dateideskriptoren vor dem Aufruf schließen someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

Das exec 3>&- 4>&-Vorher someprogschließt den Dateideskriptor, bevor es ausgeführt wird, someprogsodass someprogdiese Dateideskriptoren einfach nicht vorhanden sind.

Es kann auch so geschrieben werden: someprog 3>&- 4>&-


Schritt für Schritt Erklärung des Konstrukts:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

Von unten nach oben:

  1. Eine Subshell wird mit dem Dateideskriptor 4 erstellt, der nach stdout umgeleitet wird. Dies bedeutet, dass alles, was im Dateideskriptor 4 in der Subshell gedruckt wird, als Standard des gesamten Konstrukts ausgegeben wird.
  2. Eine Pipe wird erstellt und die Befehle links ( #part3) und rechts ( #part2) ausgeführt. exit $xsist auch der letzte Befehl der Pipe und das bedeutet, dass der String von stdin der Exit-Status des gesamten Konstrukts ist.
  3. Eine Subshell wird mit dem Dateideskriptor 3 erstellt, der nach stdout umgeleitet wird. Dies bedeutet, dass alles, was in der Dateideskriptor 3 in dieser Subshell gedruckt wird, am Ende den Beendigungsstatus #part2des gesamten Konstrukts hat.
  4. Eine Pipe wird erstellt und die Befehle links ( #part5und #part6) und rechts ( filter >&4) ausgeführt. Die Ausgabe von filterwird zu Dateideskriptor #part14 umgeleitet. In der Datei wurde Deskriptor 4 zu stdout umgeleitet. Dies bedeutet, dass die Ausgabe von filterdie Standardausgabe des gesamten Konstrukts ist.
  5. Der Beendigungsstatus von #part6wird in Dateideskriptor 3 gedruckt. In #part3Dateideskriptor 3 wurde umgeleitet #part2. Dies bedeutet, dass der Beendigungsstatus von #part6der endgültige Beendigungsstatus für das gesamte Konstrukt ist.
  6. someprogausgeführt wird. Der Exit-Status wird übernommen #part5. Die stdout wird von der Pipe aufgenommen #part4und an weitergeleitet filter. Die Ausgabe von filterwird wiederum stdout erreichen, wie in erläutert#part4

1
Nett. Für die Funktion könnte ich nur tun(read; exit $REPLY)
2.

5
(exec 3>&- 4>&-; someprog)vereinfacht zu someprog 3>&- 4>&-.
Roger Pate

Diese Methode funktioniert auch ohne Unterschalen:{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
josch

36

Während nicht genau das, was Sie gefragt haben, könnten Sie verwenden

#!/bin/bash -o pipefail

damit Ihre Rohre die letzte Rückkehr ungleich Null zurückgeben.

könnte etwas weniger codierung sein

Edit: Beispiel

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1

9
set -o pipefailinnerhalb des Skripts sollte robuster sein, zB für den Fall, dass jemand das Skript über ausführt bash foo.sh.
Maxschlepzig

Wie funktioniert das? hast du ein beispiel
Johan

2
Beachten Sie, dass -o pipefailnicht in POSIX.
scy

2
Dies funktioniert in meiner BASH 3.2.25 (1) -Release nicht. Am Anfang von / tmp / ff habe ich #!/bin/bash -o pipefail. Fehler ist:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez

2
@FelipeAlvarez: Einige Umgebungen (einschließlich Linux) Sie analysieren nicht Plätze auf #!Linien über die ersten, und so wird dies /bin/bash -o pipefail /tmp/ffanstelle der erforderlichen /bin/bash -o pipefail /tmp/ff- getopt(oder ähnlich) Analyse der Verwendung optarg, die in der Tagesordnung folgen ARGV, als Argument zu -o, so scheitert es. Wenn Sie einen Wrapper machen waren (sagen wir, bash-pfdass gerade tat exec /bin/bash -o pipefail "$@", und dass auf der Put - #!Linie, das funktionieren würde . Siehe auch: en.wikipedia.org/wiki/Shebang_%28Unix%29
Lindes

22

Was ich mache, wenn möglich, ist, den Exit-Code von fooin einzugeben bar. Wenn ich zum Beispiel weiß, dass fooniemals eine Zeile mit nur Ziffern erzeugt wird, kann ich einfach den Exit-Code eingeben:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Oder wenn ich weiß, dass die Ausgabe von foonie eine Zeile mit nur enthält .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Dies kann immer getan werden, wenn es eine Möglichkeit gibt, baralle außer der letzten Zeile zu bearbeiten und die letzte Zeile als Exit-Code zu übergeben.

Wenn bares sich um eine komplexe Pipeline handelt, deren Ausgabe Sie nicht benötigen, können Sie einen Teil davon umgehen, indem Sie den Beendigungscode in einem anderen Dateideskriptor drucken.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

Danach $exit_codesist es normalerweise foo:X bar:Y, aber es könnte sein, bar:Y foo:Xdass es barbeendet wird, bevor alle Eingaben gelesen werden, oder wenn Sie Pech haben. Ich denke, dass Schreibvorgänge in Pipes mit bis zu 512 Bytes bei allen Unices atomar sind, sodass die foo:$?und bar:$?-Teile nicht gemischt werden, solange die Tag-Zeichenfolgen weniger als 507 Bytes umfassen.

Wenn Sie die Ausgabe von erfassen müssen bar, wird es schwierig. Sie können die oben genannten Techniken kombinieren, indem Sie dafür sorgen, dass die Ausgabe von barnever eine Zeile enthält, die wie eine Exit-Code-Anzeige aussieht, die jedoch unübersichtlich wird.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

Natürlich gibt es auch die einfache Möglichkeit , den Status mithilfe einer temporären Datei zu speichern. Einfach, aber nicht so einfach in der Produktion:

  • Wenn mehrere Skripte gleichzeitig ausgeführt werden oder dasselbe Skript diese Methode an mehreren Stellen verwendet, müssen Sie sicherstellen, dass sie unterschiedliche temporäre Dateinamen verwenden.
  • Das sichere Erstellen einer temporären Datei in einem freigegebenen Verzeichnis ist schwierig. Oft /tmpist dies der einzige Ort, an dem ein Skript Dateien schreiben kann. Verwendung mktemp, die nicht POSIX ist, aber heutzutage auf allen seriösen Unices verfügbar ist.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

1
Wenn ich den temporären Dateiansatz verwende, bevorzuge ich es, einen Trap für EXIT hinzuzufügen, der alle temporären Dateien entfernt, damit auch dann kein Müll übrig bleibt, wenn das Skript stirbt
miracle173

17

Ausgehend von der Pipeline:

foo | bar | baz

Hier ist eine allgemeine Lösung, bei der nur die POSIX-Shell und keine temporären Dateien verwendet werden:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses Enthält die Statuscodes aller fehlgeschlagenen Prozesse in zufälliger Reihenfolge mit Indizes, die angeben, welcher Befehl den jeweiligen Status ausgegeben hat.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Beachten Sie die Anführungszeichen $error_statusesin meinen Tests; ohne sie grepkann man nicht unterscheiden, weil die Zeilenumbrüche zu Leerzeichen gezwungen werden.


12

Ich wollte also eine Antwort wie die von Lesmana beisteuern, aber ich denke, meine ist vielleicht eine etwas einfachere und etwas vorteilhaftere reine Bourne-Shell-Lösung:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Ich denke, dies wird am besten von innen nach außen erklärt - command1 führt seine reguläre Ausgabe aus und druckt sie auf stdout (Dateideskriptor 1). Sobald dies erledigt ist, führt printf den Exit-Code von command1 aus und druckt ihn auf seiner stdout, aber diese stdout wird umgeleitet Dateideskriptor 3.

Während Befehl1 ausgeführt wird, wird seine stdout an Befehl2 weitergeleitet (die Ausgabe von printf führt niemals zu Befehl2, da wir sie an den Dateideskriptor 3 anstelle von 1 senden, den die Pipe liest). Dann leiten wir die Ausgabe von command2 an den Dateideskriptor 4 um, so dass sie auch außerhalb des Dateideskriptors 1 bleibt - weil wir wollen, dass der Dateideskriptor 1 ein wenig später frei ist, weil wir die printf-Ausgabe des Dateideskriptors 3 wieder in den Dateideskriptor zurückführen 1 - denn genau das wird von der Befehlsersetzung (den Backticks) erfasst und in die Variable eingefügt.

Das letzte bisschen Magie ist, dass exec 4>&1wir es zuerst als separaten Befehl gemacht haben - es öffnet den Dateideskriptor 4 als Kopie des stdout der externen Shell. Die Befehlsersetzung erfasst alles, was auf Standard geschrieben ist, aus der Perspektive der darin enthaltenen Befehle. Da die Ausgabe von Befehl2 jedoch für die Befehlsersetzung den Dateideskriptor 4 enthält, wird sie von der Befehlsersetzung nicht erfasst. Sobald die Befehlsersetzung "beendet" ist, wird der gesamte Dateideskriptor 1 des Skripts angezeigt.

(Das exec 4>&1muss ein separater Befehl sein, da es vielen gängigen Shells nicht gefällt, wenn Sie versuchen, in einen Dateideskriptor innerhalb einer Befehlsersetzung zu schreiben, der in dem "externen" Befehl geöffnet wird, der die Ersetzung verwendet einfachste tragbare Möglichkeit.)

Sie können es weniger technisch und spielerisch betrachten, als ob die Ausgaben der Befehle sich gegenseitig überspringen: command1 leitet zu command2, dann springt die Ausgabe von printf über command 2, damit command2 es nicht fängt, und dann Die Ausgabe von Befehl 2 springt über die Befehlsersetzung hinaus und verlässt sie, genau wie printf gerade noch rechtzeitig landet, um von der Ersetzung erfasst zu werden, sodass sie in der Variablen endet in einer normalen Pfeife.

Soweit ich weiß, $?wird es auch weiterhin den Rückgabecode des zweiten Befehls in der Pipe enthalten, da Variablenzuweisungen, Befehlsersetzungen und zusammengesetzte Befehle für den Rückgabecode des darin enthaltenen Befehls effektiv transparent sind, also den Rückgabestatus von befehl2 sollte weitergegeben werden - dies ist meiner meinung nach eine etwas bessere lösung als die von lesmana vorgeschlagene, da keine zusätzliche funktion definiert werden muss.

Gemäß den Vorbehalten, die Lesmana erwähnt, ist es möglich, dass command1 irgendwann die Dateideskriptoren 3 oder 4 verwendet. Um also robuster zu sein, würden Sie Folgendes tun:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Beachten Sie, dass ich in meinem Beispiel zusammengesetzte Befehle verwende, aber Subshells ( ( )anstelle von { }funktionieren auch, obwohl dies möglicherweise weniger effizient ist).

Befehle erben Dateideskriptoren von dem Prozess, der sie startet, sodass die gesamte zweite Zeile den Dateideskriptor vier und der zusammengesetzte Befehl gefolgt von 3>&1den Dateideskriptor drei erbt. Das 4>&-stellt also sicher, dass der innere zusammengesetzte Befehl nicht den Dateideskriptor vier und der 3>&-Dateideskriptor drei erbt, sodass befehl1 eine 'sauberere', standardisiertere Umgebung erhält. Sie könnten auch das Innere 4>&-neben das bewegen 3>&-, aber ich denke, warum nicht einfach den Umfang so weit wie möglich einschränken.

Ich bin mir nicht sicher, wie oft die Dateideskriptoren drei und vier direkt verwendet werden - ich glaube, die meisten Programme verwenden Syscalls, die im Moment nicht verwendete Dateideskriptoren zurückgeben, aber manchmal schreibt Code direkt in Dateideskriptor 3 rate (Ich könnte mir vorstellen, dass ein Programm einen Dateideskriptor überprüft, um festzustellen, ob er geöffnet ist, und ihn verwendet, wenn er geöffnet ist, oder sich dementsprechend anders verhält, wenn er nicht geöffnet ist). Letzteres ist wahrscheinlich am besten zu beachten und für allgemeine Zwecke zu verwenden.


Sieht interessant aus, aber ich kann nicht genau herausfinden, was Sie von diesem Befehl erwarten, und mein Computer kann es auch nicht. Ich verstehe -bash: 3: Bad file descriptor.
G-Man

@ G-Man Richtig, ich vergesse immer wieder, dass Bash keine Ahnung hat, was es mit Dateideskriptoren zu tun hat, im Gegensatz zu den Shells, die ich normalerweise benutze (die Asche, die mit busybox geliefert wird). Ich werde Sie wissen lassen, wenn ich an eine Problemumgehung denke, die bash glücklich macht. Wenn Sie in der Zwischenzeit eine Debian-Box zur Hand haben, können Sie sie im Handumdrehen ausprobieren, oder wenn Sie eine Busy-Box zur Hand haben, können Sie sie mit der Busy-Box ash / sh ausprobieren.
mtraceur

@ G-Man Was ich von dem Befehl erwarte und was er in anderen Shells tut, ist, stdout von command1 umzuleiten, damit es nicht von der Befehlsersetzung erfasst wird, aber sobald es sich außerhalb der Befehlsersetzung befindet, wird es gelöscht fd3 zurück zu stdout, so dass es wie erwartet an command2 weitergeleitet wird. Wenn command1 beendet wird, wird der printf ausgelöst und gibt seinen Beendigungsstatus aus, der durch die Befehlsersetzung in der Variablen erfasst wird. Sehr detaillierte Aufschlüsselung hier: stackoverflow.com/questions/985876/tee-and-exit-status/…. Lesen Sie Ihren Kommentar auch so, als ob er eine Beleidigung darstellen sollte?
mtraceur

Wo soll ich anfangen (1) Es tut mir leid, wenn Sie sich beleidigt fühlten. "Sieht interessant aus" war ernst gemeint; Es wäre großartig, wenn etwas so Kompaktes wie das so gut funktionieren würde, wie Sie es erwartet hatten. Darüber hinaus habe ich einfach gesagt, dass ich nicht verstehe, was Ihre Lösung tun soll. Ich habe lange Zeit mit Unix gearbeitet / gespielt (seitdem es Linux gibt), und wenn ich etwas nicht verstehe, ist das eine rote Fahne , die andere Leute vielleicht auch nicht verstehen und dass es existiert braucht mehr Erklärung (IMNSHO). … (Fortsetzung)
G-Man

(Fortsetzung)… Da Sie „… gerne glauben, dass [Sie] fast alles besser verstehen als die durchschnittliche Person“, sollten Sie sich vielleicht daran erinnern, dass das Ziel von Stack Exchange nicht darin besteht, ein Kommandoschreibdienst zu sein, der ständig in Bewegung ist aus Tausenden von einmaligen Lösungen für trivial unterschiedliche Fragen; sondern Menschen beizubringen, wie sie ihre eigenen Probleme lösen können. Und zu diesem Zweck müssen Sie vielleicht die Dinge so gut erklären, dass ein „durchschnittlicher Mensch“ sie verstehen kann. Schauen Sie sich die Antwort von Lesmana an, um ein Beispiel für eine hervorragende Erklärung zu finden. … (Fortsetzung)
G-Man

11

Wenn Sie das Paket moreutils installiert haben, können Sie das Dienstprogramm mispipe verwenden, das genau das tut, was Sie gefragt haben.


7

Die obige Lösung von lesmana kann auch ohne den Mehraufwand für das Starten verschachtelter Unterprozesse mithilfe von ausgeführt werden { .. }(wobei zu beachten ist, dass diese Form von gruppierten Befehlen immer mit Semikolons abgeschlossen werden muss). Etwas wie das:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

Ich habe dieses Konstrukt mit Dash-Version 0.5.5 und Bash-Versionen 3.2.25 und 4.2.42 überprüft. Selbst wenn einige Shells keine { .. }Gruppierung unterstützen, ist es dennoch POSIX-kompatibel.


Dies funktioniert sehr gut mit den meisten Shells, mit denen ich es ausprobiert habe, einschließlich NetBSD sh, pdksh, mksh, dash, bash. Ich kann es jedoch nicht mit AT & T Ksh (93s +, 93u +) oder zsh (4.3.9, 5.2) zum Laufen bringen, auch nicht mit set -o pipefailin ksh oder einer beliebigen Anzahl gestreuter waitBefehle. Ich denke, dass es zumindest teilweise ein Parsing-Problem für ksh sein kann, als ob ich bei der Verwendung von Subshells bleibe, dann funktioniert es einwandfrei, aber selbst wenn ifman die Subshell-Variante für ksh wählt, aber die zusammengesetzten Befehle für andere belässt, schlägt dies fehl .
Greg A. Woods

4

Dies ist portabel, funktioniert also mit jeder POSIX-kompatiblen Shell, erfordert nicht, dass das aktuelle Verzeichnis beschreibbar ist, und ermöglicht die gleichzeitige Ausführung mehrerer Skripts mit demselben Trick.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Edit: Hier ist eine stärkere Version, die Gilles 'Kommentaren folgt:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: und hier ist eine etwas leichtere Variante nach dubiousjim Kommentar:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))

3
Dies funktioniert aus mehreren Gründen nicht. 1. Die temporäre Datei kann vor dem Schreiben gelesen werden. 2. Das Erstellen einer temporären Datei in einem freigegebenen Verzeichnis mit einem vorhersehbaren Namen ist unsicher (triviales DoS, Symlink Race). 3. Wenn dasselbe Skript diesen Trick mehrmals verwendet, wird immer derselbe Dateiname verwendet. Um 1 zu lösen, lesen Sie die Datei, nachdem die Pipeline abgeschlossen wurde. Verwenden Sie zur Lösung von 2 und 3 eine temporäre Datei mit einem zufällig generierten Namen oder in einem privaten Verzeichnis.
Gilles

+1 Nun, das $ {PIPESTATUS [0]} ist einfacher, aber die Grundidee hier funktioniert, wenn man die Probleme kennt, die Gilles erwähnt.
Johan

Sie können ein paar Subshells sparen: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Ich stimme zu, dass es mit Bash einfacher ist, aber in manchen Zusammenhängen lohnt es sich, zu wissen, wie man Bash vermeidet.
dubiousjim

4

Das Folgende ist als Zusatz zur Antwort von @Patrik gedacht, falls Sie eine der gebräuchlichen Lösungen nicht verwenden können.

Diese Antwort setzt Folgendes voraus:

  • Sie haben eine Muschel, die weder kennt $PIPESTATUSnoch kenntset -o pipefail
  • Sie möchten eine Pipe für die parallele Ausführung verwenden, also keine temporären Dateien.
  • Sie möchten kein zusätzliches Durcheinander haben, wenn Sie das Skript unterbrechen, möglicherweise durch einen plötzlichen Stromausfall.
  • Diese Lösung sollte relativ einfach zu befolgen und sauber zu lesen sein.
  • Sie möchten keine zusätzlichen Unterschalen einführen.
  • Sie können nicht mit den vorhandenen Dateideskriptoren fummeln, daher darf stdin / out / err nicht berührt werden (Sie können jedoch vorübergehend eine neue einführen).

Zusätzliche Annahmen. Sie können alles loswerden, aber dies überfrachtet das Rezept zu sehr, so dass es hier nicht behandelt wird:

  • Sie möchten lediglich wissen, dass alle Befehle in der PIPE den Beendigungscode 0 haben.
  • Sie benötigen keine zusätzlichen Seitenbandinformationen.
  • Ihre Shell wartet darauf, dass alle Pipe-Befehle zurückgegeben werden.

Vorher: foo | bar | bazgibt jedoch nur den Exit-Code des letzten Befehls zurück ( baz)

Gesucht: $?Muss nicht 0(true) sein, wenn einer der Befehle in der Pipe fehlgeschlagen ist

Nach:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Erklärt:

  • Ein tempfile wird mit erstellt mktemp. Dadurch wird normalerweise sofort eine Datei in erstellt/tmp
  • Diese Datei wird dann zum Schreiben auf FD 9 und zum Lesen auf FD 8 umgeleitet
  • Dann wird das tempfile sofort gelöscht. Es bleibt jedoch offen, bis beide FDs aus dem Leben geraten.
  • Jetzt wird die Pipe gestartet. Jeder Schritt wird nur dann zu FD 9 hinzugefügt, wenn ein Fehler aufgetreten ist.
  • Das waitwird für benötigt ksh, da kshsonst nicht auf den Abschluss aller Pipe-Befehle gewartet wird. Beachten Sie jedoch, dass es unerwünschte Nebeneffekte gibt, wenn Hintergrundaufgaben vorhanden sind. Daher habe ich diese standardmäßig auskommentiert. Wenn das Warten nicht schmerzt, können Sie es in kommentieren.
  • Anschließend wird der Inhalt der Datei gelesen. Wenn es leer ist (weil alles funktioniert) readzurückkehrt false, so truezeigt einen Fehler an

Dies kann als Plugin-Ersatz für einen einzelnen Befehl verwendet werden und erfordert nur Folgendes:

  • Unbenutzte FDs 9 und 8
  • Eine einzelne Umgebungsvariable, die den Namen der Datei enthält
  • Und dieses Rezept kann an jede beliebige Shell angepasst werden, die eine E / A-Umleitung ermöglicht
  • Außerdem ist es ziemlich plattformunabhängig und benötigt keine Dinge wie /proc/fd/N

BUGs:

Dieses Skript hat einen Fehler, falls /tmpder Speicherplatz knapp wird. Wenn Sie Schutz gegen diese künstlichen Fall brauchen auch, können Sie es tun , wie folgt, was jedoch den Nachteil hat, dass die Anzahl der 0in 000der Anzahl von Befehlen in der Leitung hängt, so ist es etwas komplizierter:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Hinweise zur Mobilität:

  • kshund ähnliche Shells, die nur auf den letzten Pipe-Befehl warten, brauchen das waitunkommentierte

  • Das letzte Beispiel wird printf "%1s" "$?"anstelle von verwendet, echo -n "$?"da dies portabler ist. Nicht jede Plattform interpretiert -nrichtig.

  • printf "$?"würde es auch tun, printf "%1s"fängt jedoch einige Eckfälle ab, falls Sie das Skript auf einer wirklich kaputten Plattform ausführen. (Lesen Sie: wenn Sie gerade programmieren paranoia_mode=extreme.)

  • FD 8 und FD 9 können auf Plattformen, die mehrere Ziffern unterstützen, höher sein. AFAIR eine POSIX-konforme Shell muss nur einzelne Ziffern unterstützen.

  • War 8.2 mit Debian getestet sh, bash, ksh, ash, sashund sogarcsh


3

Mit ein wenig Vorsicht sollte dies funktionieren:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

Wie wäre es, wie jlliagre aufzuräumen? Hinterlassen Sie keine Datei mit dem Namen foo-status?
Johan

@Johan: Wenn Sie meinen Vorschlag bevorzugen, zögern Sie nicht, ihn abzustimmen;) Zusätzlich zum Verzicht auf das Hinterlassen einer Datei hat dies den Vorteil, dass mehrere Prozesse dies gleichzeitig ausführen können und das aktuelle Verzeichnis nicht beschreibbar sein muss.
Juli

2

Der folgende 'if'-Block wird nur ausgeführt, wenn' command 'erfolgreich war:

if command; then
   # ...
fi

Im Einzelnen können Sie Folgendes ausführen:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Welches läuft haconf -makerwund speichert seine stdout und stderr zu "$ haconf_out". Wenn der zurückgegebene Wert von haconftrue ist, wird der ' grepif' -Block ausgeführt und lautet "$ haconf_out", wobei versucht wird, ihn mit "Cluster bereits beschreibbar" abzugleichen.

Beachten Sie, dass sich die Rohre automatisch selbst reinigen. Bei der Umleitung müssen Sie vorsichtig sein, um "$ haconf_out" zu entfernen, wenn Sie fertig sind.

Nicht so elegant wie pipefail, aber eine legitime Alternative, wenn diese Funktionalität nicht in Reichweite ist.


1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$

0

(Mindestens mit bash) kann in Kombination mit set -esubshell verwendet werden, um Pipefail explizit zu emulieren und bei einem Pipe-Fehler zu beenden

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Wenn foodies aus irgendeinem Grund fehlschlägt, wird der Rest des Programms nicht ausgeführt und das Skript wird mit dem entsprechenden Fehlercode beendet. (Dies setzt voraus, dass fooein eigener Fehler ausgegeben wird, der ausreicht, um die Fehlerursache zu verstehen.)


-1

BEARBEITEN : Diese Antwort ist falsch, aber interessant, also lasse ich sie zum späteren Nachschlagen.


Durch Hinzufügen von a !zum Befehl wird der Rückkehrcode invertiert.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #

1
Ich denke, das hat nichts damit zu tun. In Ihrem Beispiel möchte ich den Exit-Code von kennen ls, nicht den Exit-Code von invertierenbogus_command
Michael Mrozek

2
Ich schlage vor, diese Antwort zurückzuziehen.
maxschlepzig

3
Nun, es scheint, ich bin ein Idiot. Ich habe dies tatsächlich in einem Skript verwendet, bevor ich dachte, es würde das tun, was das OP wollte. Gut, dass ich es für nichts Wichtiges benutzt habe
Falmarri
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.