Pipe-Ausgabe und Capture-Exit-Status in Bash

418

Ich möchte einen Befehl mit langer Laufzeit in Bash ausführen und sowohl den Exit-Status erfassen als auch die Ausgabe abschlagen .

Also mache ich das:

command | tee out.txt
ST=$?

Das Problem ist, dass die Variable ST den Exit-Status von teeund nicht von Befehl erfasst . Wie kann ich das lösen?

Beachten Sie, dass der Befehl lange ausgeführt wird und das Umleiten der Ausgabe in eine Datei zur späteren Anzeige keine gute Lösung für mich ist.

Flybywire
quelle
1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Übereinstimmung - Fehler gefunden" || echo -e "Keine Übereinstimmung - alles gut" Dies testet alle Werte des Arrays auf einmal und gibt eine Fehlermeldung aus, wenn einer der zurückgegebenen Pipe-Werte nicht Null ist. Dies ist eine ziemlich robuste allgemeine Lösung zum Erkennen von Fehlern in einer Rohrleitungssituation.
Brian S. Wilson
unix.stackexchange.com/questions/14270/…
Ciro Santilli 法轮功 冠状 冠状 六四 事件 法轮功

Antworten:

517

Es gibt eine interne Bash-Variable namens $PIPESTATUS; Es ist ein Array, das den Exit-Status jedes Befehls in Ihrer letzten Vordergrund-Pipeline von Befehlen enthält.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Oder eine andere Alternative, die auch mit anderen Shells (wie zsh) funktioniert, wäre das Aktivieren von Pipefail:

set -o pipefail
...

Die erste Option ist nicht mit der Arbeit zshauf ein wenig wegen Bit andere Syntax.

codar
quelle
21
Hier finden Sie eine gute Erklärung mit Beispielen für PIPESTATUS UND Pipefail: unix.stackexchange.com/a/73180/7453 .
Slm
18
Hinweis: $ PIPESTATUS [0] enthält den Exit-Status des ersten Befehls in der Pipe, $ PIPESTATUS [1] den Exit-Status des zweiten Befehls usw.
Simpleuser
18
Natürlich müssen wir uns daran erinnern, dass dies Bash-spezifisch ist: Wenn ich (zum Beispiel) ein Skript schreiben würde, das auf der "sh" -Implementierung von BusyBox auf meinem Android-Gerät oder auf einer anderen eingebetteten Plattform mit einem anderen "sh" ausgeführt werden soll. Variante würde dies nicht funktionieren.
Asfand Qazi
4
Für diejenigen, die über die Erweiterung der nicht zitierten Variablen besorgt sind: Der Exit-Status ist in Bash immer eine vorzeichenlose 8-Bit-Ganzzahl , daher ist es nicht erforderlich, ihn in Anführungszeichen zu setzen. Dies gilt im Allgemeinen auch unter Unix, wo der Exit-Status explizit als 8-Bit definiert ist und selbst von POSIX selbst als nicht signiert angenommen wird, z. B. wenn die logische Negation definiert wird .
Palec
3
Sie können auch verwenden exit ${PIPESTATUS[0]}.
Chaoran
142

Die Verwendung von Bashs set -o pipefailist hilfreich

Pipefail: Der Rückgabewert einer Pipeline ist der Status des letzten Befehls, der mit einem Status ungleich Null beendet wird, oder Null, wenn kein Befehl mit einem Status ungleich Null beendet wurde

Felipe Alvarez
quelle
23
Falls Sie die Pipefail-Einstellung des gesamten Skripts nicht ändern möchten, können Sie die Option nur lokal festlegen:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan
7
@Jaan Dies würde eine Subshell ausführen. Wenn Sie dies vermeiden möchten, können Sie set -o pipefailden Befehl set +o pipefailausführen und anschließend die Option deaktivieren.
Linus Arver
2
Hinweis: Das Fragenposter möchte keinen "allgemeinen Exit-Code" der Pipe, sondern den Rückkehrcode "Befehl". Mit -o pipefailwürde er wissen, ob die Pipe ausfällt, aber wenn sowohl 'Befehl' als auch 'Abschlag' fehlschlagen, würde er den Exit-Code von 'Abschlag' erhalten.
t0r0X
@LinusArver würde das den Exit-Code nicht löschen, da es ein Befehl ist, der erfolgreich ist?
carlin.scott
126

Dumme Lösung: Verbinden Sie sie über eine Named Pipe (mkfifo). Dann kann der Befehl als zweiter ausgeführt werden.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
EFraim
quelle
20
Dies ist die einzige Antwort in dieser Frage, die auch für die einfache sh Unix-Shell funktioniert . Vielen Dank!
JamesThomasMoon1979
3
@ DaveKennedy: Dumm wie in "offensichtlich, erfordert keine komplizierten Kenntnisse der Bash-Syntax"
EFraim
10
Während die Bash-Antworten eleganter sind, wenn Sie die zusätzlichen Funktionen von Bash nutzen, ist dies die plattformübergreifendere Lösung. Es ist auch etwas, über das man im Allgemeinen nachdenken sollte, da jedes Mal, wenn Sie einen Befehl mit langer Laufzeit ausführen, eine Namenspipe oft der flexibelste Weg ist. Es ist erwähnenswert, dass einige Systeme keine haben mkfifound stattdessen benötigen, mknod -pwenn ich mich richtig erinnere.
Haravikk
3
Manchmal gibt es beim Stapelüberlauf Antworten, die Sie hundertmal positiv bewertet haben, damit die Leute aufhören, andere Dinge zu tun, die keinen Sinn ergeben. Dies ist eine davon. Danke mein Herr.
Dan Chase
1
Für den Fall, dass jemand ein Problem mit mkfifooder hat mknod -p: In meinem Fall war der richtige Befehl zum Erstellen einer Pipe-Datei mknod FILE_NAME p.
Karol Gil
36

Es gibt ein Array, das Ihnen den Exit-Status jedes Befehls in einer Pipe gibt.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1
Stefano Borini
quelle
26

Diese Lösung funktioniert ohne Verwendung von Bash-spezifischen Funktionen oder temporären Dateien. Bonus: Am Ende ist der Exit-Status tatsächlich ein Exit-Status und keine Zeichenfolge in einer Datei.

Situation:

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

echo $?

In meiner Antwort auf dieselbe Frage auf unix.stackexchange.com finden Sie eine ausführliche Erklärung und eine Alternative ohne Unterschalen und einige Einschränkungen.

Lesmana
quelle
20

Durch Kombinieren PIPESTATUS[0]und das Ergebnis der Ausführung des exitBefehls in einer Subshell können Sie direkt auf den Rückgabewert Ihres ursprünglichen Befehls zugreifen:

command | tee ; ( exit ${PIPESTATUS[0]} )

Hier ist ein Beispiel:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

werde dir geben:

return value: 1

Par
quelle
4
Vielen Dank, dies hat mir erlaubt, das Konstrukt zu verwenden: VALUE=$(might_fail | piping)das PIPESTATUS nicht in der Master-Shell setzt, sondern dessen Fehlerlevel setzt. Mit: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})Ich will wollen, ich wollte.
Vaab
@vaab, diese Syntax sieht wirklich gut aus, aber ich bin verwirrt darüber, was "Piping" in Ihrem Kontext bedeutet. Ist das genau der Ort, an dem man 'tee' oder was auch immer für die Ausgabe von may_fail verarbeitet? ty!
AnneTheAgile
1
@AnneTheAgile 'piping' steht in meinem Beispiel für Befehle, von denen Sie die errlvl nicht sehen möchten. Zum Beispiel: eine oder eine beliebige Kombination von 'tee', 'grep', 'sed', ... Es ist nicht so ungewöhnlich, dass diese Piping-Befehle Informationen aus einer größeren Ausgabe oder Protokollausgabe des Hauptgeräts formatieren oder extrahieren sollen Befehl: Sie interessieren sich dann mehr für die Fehlerstufe des Hauptbefehls (die, die ich in meinem Beispiel 'may_fail' genannt habe), aber ohne mein Konstrukt gibt die gesamte Zuweisung die Errlvl des letzten Pipeline-Befehls zurück, die hier bedeutungslos ist. Ist das klarer?
Vaab
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}falls nicht tee sondern grep
filtert
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 lässt sich am besten von innen nach außen erklären - Befehl1 wird ausgeführt und druckt seine reguläre Ausgabe auf stdout (Dateideskriptor 1). Sobald dies erledigt ist, wird printf den Exit-Code von icommand1 ausführen und auf seinem stdout drucken, aber dieser stdout wird umgeleitet Dateideskriptor 3.

Während Befehl1 ausgeführt wird, wird seine Standardausgabe an Befehl2 weitergeleitet (die Ausgabe von printf gelangt nie zu Befehl2, da wir sie an Dateideskriptor 3 anstatt an 1 senden, was die Pipe liest). Dann leiten wir die Ausgabe von command2 in den Dateideskriptor 4 um, so dass sie auch außerhalb des Dateideskriptors 1 bleibt - weil wir den Dateideskriptor 1 für ein wenig später frei haben möchten, weil wir die printf-Ausgabe auf Dateideskriptor 3 wieder in den Dateideskriptor zurückbringen 1 - weil dies die Befehlssubstitution (die Backticks) erfasst und in die Variable eingefügt wird.

Das letzte bisschen Magie ist, dass exec 4>&1wir es zuerst als separaten Befehl getan haben - es öffnet den Dateideskriptor 4 als Kopie des Standardout der externen Shell. Die Befehlssubstitution erfasst alles, was auf Standard geschrieben ist, aus der Perspektive der darin enthaltenen Befehle. Da jedoch die Ausgabe von Befehl2 in Bezug auf die Befehlssubstitution in den Dateideskriptor 4 geht, wird sie durch die Befehlssubstitution nicht erfasst - jedoch einmal Wenn die Befehlssubstitution "beendet" wird, wird effektiv immer noch der gesamte Dateideskriptor 1 des Skripts aufgerufen.

(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 Befehlssubstitution zu schreiben, der im "externen" Befehl geöffnet wird, der die Substitution verwendet einfachste tragbare Möglichkeit, dies zu tun.)

Sie können es weniger technisch und spielerisch betrachten, als ob sich die Ausgaben der Befehle gegenseitig überspringen: Befehl1 leitet zu Befehl2 weiter, dann springt die Ausgabe des Drucks über Befehl 2, sodass Befehl2 ihn nicht abfängt, und dann Die Ausgabe von Befehl 2 springt über die Befehlssubstitution hinaus und verlässt sie, sobald printf gerade rechtzeitig landet, um von der Substitution erfasst zu werden, sodass sie in der Variablen landet, und die Ausgabe von Befehl2 wird auf fröhliche Weise in die Standardausgabe geschrieben in einem normalen Rohr.

Soweit ich weiß, $?wird der Rückkehrcode des zweiten Befehls in der Pipe weiterhin enthalten sein, da Variablenzuweisungen, Befehlsersetzungen und zusammengesetzte Befehle für den Rückkehrcode des darin enthaltenen Befehls effektiv transparent sind, sodass der Rückgabestatus von Befehl2 sollte verbreitet werden - dies und das Fehlen einer zusätzlichen Funktion ist der Grund, warum ich denke, dass dies eine etwas bessere Lösung sein könnte als die von Lesmana vorgeschlagene.

Gemäß den Vorbehalten, die Lesmana erwähnt, ist es möglich, dass Befehl1 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 (die Verwendung von ( )anstelle von { }funktioniert 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 erbt und der zusammengesetzte Befehl gefolgt von 3>&1dem Dateideskriptor drei. Das 4>&-stellt also sicher, dass der innere zusammengesetzte Befehl den Dateideskriptor vier und den 3>&-Dateideskriptor drei nicht erbt, sodass command1 eine sauberere, standardisiertere Umgebung erhält. Sie könnten auch das Innere 4>&-neben das verschieben 3>&-, aber ich denke, warum nicht einfach den Umfang so weit wie möglich einschränken.

Ich bin mir nicht sicher, wie oft die Dateien den Dateideskriptor drei und vier direkt verwenden. Ich denke, die meisten Programme verwenden Syscalls, die derzeit nicht verwendete Dateideskriptoren zurückgeben, aber manchmal schreibt Code direkt in den Dateideskriptor 3, I. Vermutung (Ich könnte mir vorstellen, dass ein Programm einen Dateideskriptor überprüft, um festzustellen, ob er geöffnet ist, und ihn verwendet, wenn dies der Fall ist, oder sich entsprechend anders verhält, wenn dies nicht der Fall ist). Letzteres ist daher wahrscheinlich am besten zu beachten und für allgemeine Fälle zu verwenden.

mtraceur
quelle
Schöne Erklärung!
Selurvedu
6

In Ubuntu und Debian können Sie apt-get install moreutils. Dies enthält ein Hilfsprogramm namens mispipe, das den Exit-Status des ersten Befehls in der Pipe zurückgibt.

Bryan Larsen
quelle
5
(command | tee out.txt; exit ${PIPESTATUS[0]})

Im Gegensatz zur Antwort von @ cODAR gibt dies den ursprünglichen Exit-Code des ersten Befehls zurück und nicht nur 0 für Erfolg und 127 für Fehler. Aber wie @Chaoran betonte, können Sie einfach anrufen ${PIPESTATUS[0]}. Es ist jedoch wichtig, dass alles in Klammern steht.

jakob-r
quelle
4

Außerhalb von Bash können Sie Folgendes tun:

bash -o pipefail  -c "command1 | tee output"

Dies ist beispielsweise in Ninja-Skripten nützlich, in denen die Shell erwartet wird /bin/sh.

Anthony Scemama
quelle
3

PIPESTATUS [@] muss unmittelbar nach der Rückkehr des Pipe-Befehls in ein Array kopiert werden. Alle Lesevorgänge von PIPESTATUS [@] löschen den Inhalt. Kopieren Sie es in ein anderes Array, wenn Sie den Status aller Pipe-Befehle überprüfen möchten. "$?" ist der gleiche Wert wie das letzte Element von "$ {PIPESTATUS [@]}", und das Lesen scheint "$ {PIPESTATUS [@]}" zu zerstören, aber ich habe dies nicht absolut überprüft.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Dies funktioniert nicht, wenn sich das Rohr in einer Unterschale befindet. Eine Lösung für dieses Problem finden
Sie unter bash pipestatus im Befehl backticked?

maxdev137
quelle
3

Der einfachste Weg, dies in einfacher Bash zu tun, ist die Verwendung der Prozesssubstitution anstelle einer Pipeline eine . Es gibt verschiedene Unterschiede, die für Ihren Anwendungsfall jedoch wahrscheinlich keine große Rolle spielen:

  • Beim Ausführen einer Pipeline wartet bash, bis alle Prozesse abgeschlossen sind.
  • Wenn Sie Strg-C an bash senden, werden alle Prozesse einer Pipeline abgebrochen, nicht nur der Hauptprozess.
  • Die pipefailOption und diePIPESTATUS Variable sind für die Prozessersetzung irrelevant.
  • Möglicherweise mehr

Mit der Prozessersetzung startet bash den Prozess einfach und vergisst ihn, er ist nicht einmal sichtbar in jobs .

Erwähnte Unterschiede beiseite, consumer < <(producer)undproducer | consumer sind im Wesentlichen gleichwertig.

Wenn Sie umdrehen möchten, welcher der "Haupt" -Prozess ist, drehen Sie einfach die Befehle und die Richtung der Ersetzung um producer > >(consumer). In Ihrem Fall:

command > >(tee out.txt)

Beispiel:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

Wie gesagt, es gibt Unterschiede zum Pipe-Ausdruck. Der Prozess hört möglicherweise nie auf zu laufen, es sei denn, er reagiert empfindlich auf das Schließen des Rohrs. Insbesondere kann es sein, dass weiterhin Dinge in Ihr Standard geschrieben werden, was verwirrend sein kann.

klackern
quelle
1

Reine Schalenlösung:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

Und jetzt mit dem zweiten catersetzt durch false:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

Bitte beachten Sie, dass die erste Katze ebenfalls ausfällt, da sie standardmäßig geschlossen wird. Die Reihenfolge der fehlgeschlagenen Befehle im Protokoll ist in diesem Beispiel korrekt, aber verlassen Sie sich nicht darauf.

Diese Methode ermöglicht das Erfassen von stdout und stderr für die einzelnen Befehle, sodass Sie diese dann auch in eine Protokolldatei kopieren können, wenn ein Fehler auftritt, oder sie einfach löschen können, wenn kein Fehler vorliegt (wie die Ausgabe von dd).

Coroos
quelle
1

Basis ist die Antwort von @ brian-s-wilson; diese Bash-Helfer-Funktion:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

verwendet also:

1: get_bad_things muss erfolgreich sein, sollte aber keine Ausgabe erzeugen; aber wir wollen die Ausgabe sehen, die es produziert

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: Alle Pipelines müssen erfolgreich sein

thing | something -q | thingy
pipeinfo || return
Sam Liddicott
quelle
1

Es kann manchmal einfacher und klarer sein, einen externen Befehl zu verwenden, als sich mit den Details von Bash zu befassen. Die Pipeline wird aus der minimalen Prozessskriptsprache execline mit dem Rückkehrcode des zweiten Befehls * beendet, genau wie eine shPipeline, aber im Gegensatz shdazu ermöglicht sie das Umkehren der Richtung der Pipe, sodass wir den Rückkehrcode des Produzenten erfassen können Prozess (das Folgende ist alles in der shBefehlszeile, aber mit execlineinstalliert):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

Die Verwendung pipelinehat dieselben Unterschiede zu nativen Bash-Pipelines wie die in Antwort Nr. 43972501 verwendete Bash-Prozess-Substitution .

* Beendet eigentlich pipelineüberhaupt nicht, es sei denn, es liegt ein Fehler vor. Es wird im zweiten Befehl ausgeführt, daher ist es der zweite Befehl, der die Rückgabe ausführt.

klackern
quelle