webentwicklung-frage-antwort-db.com.de

Pipe-Ausgabe und Erfassung des Exit-Status in Bash

Ich möchte einen Befehl mit langer Laufzeit in Bash ausführen und sowohl dessen Beendigungsstatus als auch tee dessen Ausgabe erfassen.

Also mache ich das:

command | tee out.txt
ST=$?

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

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

384
flybywire

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

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

Oder eine andere Alternative, die auch mit anderen Shells (wie zsh) funktioniert, ist die Aktivierung von Pipefail:

set -o pipefail
...

Die erste Option funktioniert nicht mit zsh aufgrund einer etwas anderen Syntax.

480
codar

mit bashs set -o pipefail ist hilfreich

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

137
Felipe Alvarez

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

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
116
EFraim

Es gibt ein Array, das den Beendigungsstatus jedes Befehls in einer Pipe angibt.

$ 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
35
Stefano Borini

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 Beendigungsstatus von someprog und die Ausgabe von filter.

Hier ist meine Lösung:

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

echo $?

Siehe meine Antwort auf dieselbe Frage auf unix.stackexchange.com für eine detaillierte Erklärung und eine Alternative ohne Unterschalen und einige Einschränkungen.

25
lesmana

Durch Kombinieren von PIPESTATUS[0] Und dem Ergebnis der Ausführung des Befehls exit 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

19
par

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 icommand1 aus und druckt ihn auf seinem stdout, aber dieser 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 wir zuerst exec 4>&1 Als separaten Befehl ausgeführt 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 - aber da die Ausgabe von Befehl2 in Bezug auf die Befehlsersetzung in den Dateideskriptor 4 verschoben wird, erfasst sie die Befehlsersetzung nicht - jedoch einmal Raus aus der Befehlsersetzung, geht es effektiv immer noch zum gesamten Dateideskriptor 1 des Skripts.

(Das exec 4>&1 Muss ein separater Befehl sein, da es vielen gebräuchlichen Shells nicht gefällt, wenn Sie versuchen, in einen Dateideskriptor innerhalb einer Befehlsersetzung zu schreiben, die mit dem Befehl "external" geöffnet wird, der den Befehl verwendet Dies ist also der einfachste tragbare Weg, dies zu tun.)

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, und die Ausgabe von Befehl 2 wird auf fröhliche Weise in die Standardausgabe geschrieben, genauso wie in einer normalen Pfeife.

Soweit ich weiß, enthält $? Weiterhin den Rückkehrcode des zweiten Befehls in der Pipe, da Variablenzuweisungen, Befehlsersetzungen und zusammengesetzte Befehle für den Rückkehrcode des darin enthaltenen Befehls transparent sind Der Rückgabestatus von command2 sollte also 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 (die Verwendung von ( ) Anstelle von { } Funktioniert ebenfalls, ist jedoch möglicherweise weniger effizient.)

Befehle erben Dateideskriptoren von dem Prozess, der sie startet, sodass die gesamte zweite Zeile den Dateideskriptor vier und der zusammengesetzte Befehl gefolgt von 3>&1 Den Dateideskriptor drei erbt. Der Befehl 4>&- Stellt also sicher, dass der innere zusammengesetzte Befehl den Dateideskriptor vier und der Befehl 3>&- Den Dateideskriptor drei nicht erbt, sodass befehl1 eine 'sauberere', standardisiertere Umgebung erhält. Sie können auch den inneren 4>&- Neben den 3>&- Bewegen, aber ich denke, warum schränken Sie den Bereich nicht so weit wie möglich ein.

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.

10
mtraceur

In Ubuntu und Debian können Sie apt-get install moreutils. Dieses enthält ein Dienstprogramm mit dem Namen mispipe, das den Beendigungsstatus des ersten Befehls in der Pipe zurückgibt.

6
Bryan Larsen

PIPESTATUS [@] muss unmittelbar nach der Rückkehr des Pipe-Befehls in ein Array kopiert werden. Bei jedem Lesevorgang von PIPESTATUS [@] wird der Inhalt gelöscht. Kopieren Sie es in ein anderes Array, wenn Sie den Status aller Pipe-Befehle überprüfen möchten. "$?" ist derselbe 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 die Pipe in einer Unterschale befindet. Für eine Lösung für dieses Problem,
siehe bash pipestatus im Befehl backticked?

3
maxdev137
(command | tee out.txt; exit ${PIPESTATUS[0]})

Anders als bei der Antwort von @ cODAR wird der ursprüngliche Exit-Code des ersten Befehls zurückgegeben und nicht nur 0 für Erfolg und 127 für Misserfolg. Aber wie @Chaoran betonte, können Sie einfach ${PIPESTATUS[0]} Aufrufen. Es ist jedoch wichtig, dass alles in Klammern gesetzt wird.

3
jakob-r

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

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

Dies ist beispielsweise in Ninja-Skripten nützlich, bei denen die Shell voraussichtlich /bin/sh Lautet.

2
Anthony Scemama

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

  • Beim Ausführen einer Pipeline wartet die Bash, bis alle Prozesse abgeschlossen sind.
  • Durch das Senden von Strg-C an Bash werden alle Prozesse einer Pipeline beendet, nicht nur der Hauptprozess.
  • Die Option pipefail und die Variable PIPESTATUS sind für die Verarbeitung der Substitution nicht relevant.
  • Möglicherweise mehr

Bei der Prozessersetzung startet bash den Prozess nur und vergisst, dass er nicht einmal in jobs sichtbar ist.

Abgesehen von den genannten Unterschieden sind consumer < <(producer) und producer | consumer Im Wesentlichen gleichwertig.

Wenn Sie den "Haupt" -Prozess spiegeln möchten, spiegeln Sie einfach die Befehle und die Richtung der Ersetzung in producer > >(consumer). In deinem 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 es weiterhin Dinge an Ihre Standardausgabe schreibt, was verwirrend sein kann.

2
clacke

Gestützt auf 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 so:

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
1
Sam Liddicott

Manchmal ist es einfacher und klarer, einen externen Befehl zu verwenden, als sich mit den Details von bash zu befassen. pipeline , ausgehend von der minimalen Skriptsprache für Prozesse execline , wird mit dem Rückkehrcode des zweiten Befehls * beendet, genau wie bei einer sh -Pipeline, jedoch anders als bei sh, erlaubt das Umkehren der Richtung der Pipe, so dass wir den Rückgabecode des Producer-Prozesses erfassen können (das Folgende steht alles in der sh Befehlszeile, aber mit execline Eingerichtet):

$ # 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 von pipeline unterscheidet sich von nativen Bash-Pipelines genauso wie die in answer # 43972501 verwendete Bash-Prozessersetzung.

* Tatsächlich wird pipeline erst beendet, wenn ein Fehler vorliegt. Es wird in den zweiten Befehl ausgeführt, also ist es der zweite Befehl, der die Rückgabe ausführt.

1
clacke

Pure Shell Lö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 cat ersetzt 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 auch die erste Katze ausfällt, weil sie nicht mehr funktioniert. Die Reihenfolge der fehlgeschlagenen Befehle im Protokoll ist in diesem Beispiel korrekt, aber verlassen Sie sich nicht darauf.

Diese Methode ermöglicht die Erfassung von stdout und stderr für die einzelnen Befehle, sodass Sie diese im Fehlerfall auch in eine Protokolldatei sichern oder einfach löschen können, wenn kein Fehler vorliegt (wie die Ausgabe von dd).

1
Coroos