Ist es möglich, stdout und stderr in verschiedenen Variablen zu speichern oder zu erfassen , ohne eine temporäre Datei zu verwenden? Im Moment mache ich das, um beim Ausführen stdout in out
und stderr in zu bekommen , aber ich möchte die temporäre Datei vermeiden.err
some_command
error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file
ksh -c 'function f { echo out; echo err >&2; }; x=${ { y=$(f); } 2>&1;}; typeset -p x y'
Antworten:
Ok, es wurde ein bisschen hässlich, aber hier ist eine Lösung:
unset t_std t_err eval "$( (echo std; echo err >&2) \ 2> >(readarray -t t_err; typeset -p t_err) \ > >(readarray -t t_std; typeset -p t_std) )"
wo
(echo std; echo err >&2)
muss durch den eigentlichen Befehl ersetzt werden. Die Ausgabe von stdout wird$t_std
zeilenweise im Array gespeichert , wobei die Zeilenumbrüche (the-t
) und stderr weggelassen werden$t_err
.Wenn Sie keine Arrays mögen, können Sie dies tun
unset t_std t_err eval "$( (echo std; echo err >&2 ) \ 2> >(t_err=$(cat); typeset -p t_err) \ > >(t_std=$(cat); typeset -p t_std) )"
was so ziemlich das Verhalten von nachahmt,
var=$(cmd)
bis auf den Wert, der$?
uns zur letzten Modifikation führt:unset t_std t_err t_ret eval "$( (echo std; echo err >&2; exit 2 ) \ 2> >(t_err=$(cat); typeset -p t_err) \ > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"
Hier
$?
ist in erhalten$t_ret
Getestet auf Debian Wheezy mit GNU
bash
, Version 4.2.37 (1) -release (i486-pc-linux-gnu) .quelle
eval "$( eval "$@" 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"; exit $t_ret
typeset -p t_out
undtypeset -p t_err
könnte gemischt sein, wodurch die Ausgabe unbrauchbar wird.>>()
stattdessen> >()
. Ersteres ist ein No-Go in Bash; In Zsh wird der Prozessersetzungsteil korrekt analysiert, manchmal jedoch eine verstümmelte Ausgabe ausgegeben. Ich weiß nicht warum,> >()
scheint aber zuverlässig zu funktionieren. Ich bin immer noch nicht ganz überzeugt.typeset -p
ist definitiv nicht atomar, oder?Um es zusammenzufassen alles bis zum Nutzen des Lesers, hier ist ein
Einfache wiederverwendbare
bash
LösungDiese Version verwendet Subshells und läuft ohne
tempfile
s. (Einetempfile
Version, die ohne Subshells ausgeführt wird, finden Sie in meiner anderen Antwort .): catch STDOUT STDERR cmd args.. catch() { eval "$({ __2="$( { __1="$("${@:3}")"; } 2>&1; ret=$?; printf '%q=%q\n' "$1" "$__1" >&2; exit $ret )" ret="$?"; printf '%s=%q\n' "$2" "$__2" >&2; printf '( exit %q )' "$ret" >&2; } 2>&1 )"; }
Anwendungsbeispiel:
dummy() { echo "$3" >&2 echo "$2" >&1 return "$1" } catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n data \n\n' printf 'ret=%q\n' "$?" printf 'stdout=%q\n' "$stdout" printf 'stderr=%q\n' "$stderr"
Dies wird gedruckt
ret=3 stdout=$'\ndiffcult\n data ' stderr=$'\nother\n difficult \n data '
So kann es verwendet werden, ohne tiefer darüber nachzudenken. Einfach
catch VAR1 VAR2
vor jeden stellencommand args..
und fertig.Einige
if cmd args..; then
werden werdenif catch VAR1 VAR2 cmd args..; then
. Wirklich nichts komplexes.Diskussion
F: Wie funktioniert es?
Es packt nur Ideen aus den anderen Antworten hier in eine Funktion ein, so dass sie leicht wiederverwendet werden können.
catch()
Grundsätzlich verwendeteval
, um die beiden Variablen zu setzen. Dies ähnelt https://stackoverflow.com/a/18086548Betrachten Sie einen Anruf von
catch out err dummy 1 2a 3b
:Lassen Sie uns das
eval "$({
und das__2="$(
für jetzt überspringen . Ich werde später darauf zurückkommen.__1="$("$("${@:3}")"; } 2>&1;
führt es ausdummy 1 2a 3b
und speichert esstdout
in__1
für die spätere Verwendung. So__1
wird2a
. Es leitet auchstderr
vondummy
zu umstdout
, so dass sich der äußere Fang sammeln kannstdout
ret=$?;
fängt den Exit-Code ab, der ist1
printf '%q=%q\n' "$1" "$__1" >&2;
gibt dannout=2a
an ausstderr
.stderr
hier verwendet wird, da der Stromstdout
bereits über die Rolle übernommen hatstderr
dendummy
Befehls.exit $ret
Leiten Sie dann den Exit-Code (1
) an die nächste Stufe weiter.Nun zum Äußeren
__2="$( ... )"
:Dies fängt
stdout
das Obige, dasstderr
derdummy
Aufruf ist, in eine Variable auf__2
. (Wir könnten hier wiederverwenden__1
, aber ich habe__2
es früher weniger verwirrend gemacht.) So__2
wird3b
ret="$?";
fängt den (zurückgegebenen) Rückkehrcode1
(vondummy
) erneut abprintf '%s=%q\n' "$2" "$__2" >&2;
gibt dannerr=3a
an ausstderr
.stderr
wird erneut verwendet, da es bereits zur Ausgabe der anderen Variablen verwendet wurdeout=2a
.printf '( exit %q )' "$ret" >&2;
gibt dann den Code aus, um den richtigen Rückgabewert einzustellen. Ich habe keinen besseren Weg gefunden, da das Zuweisen zu einer Variablen einen Variablennamen erfordert, der dann nicht als erstes oder zweites Argument verwendet werden kanncatch
.Bitte beachten Sie, dass wir als Optimierung diese 2 auch
printf
als einzelne wieprintf '%s=%q\n( exit %q )
"$ __ 2" "$ ret" ` schreiben könnten .Was haben wir bisher?
Wir haben folgendes an stderr geschrieben:
out=2a err=3b ( exit 1 )
wo
out
aus ist$1
,2a
ist ausstdout
demdummy
,err
aus$2
,3b
ist vonstderr
dendummy
, und das1
ist von dem Rückkehrcodedummy
.Bitte beachten Sie, dass
%q
im Format vonprintf
für das Zitieren gesorgt wird, sodass die Shell die richtigen (einzelnen) Argumente sieht, wenn es darum gehteval
.2a
und3b
sind so einfach, dass sie buchstäblich kopiert werden.Nun zum Äußeren
eval "$({ ... } 2>&1 )";
:Dies führt alle oben genannten aus, die die 2 Variablen und die ausgeben
exit
, sie (dafür die2>&1
) abfangen und sie mit in die aktuelle Shell analysiereneval
.Auf diese Weise werden die 2 Variablen und auch der Rückkehrcode gesetzt.
F: Es verwendet,
eval
was böse ist. Ist es also sicher?printf %q
es keine Fehler gibt, sollte es sicher sein. Aber Sie müssen immer sehr vorsichtig sein, denken Sie nur an ShellShock.F: Fehler?
Es sind keine offensichtlichen Fehler bekannt, außer den folgenden:
Das Abfangen großer Ausgaben erfordert viel Speicher und CPU, da alles in Variablen geht und von der Shell zurückparst werden muss. Verwenden Sie es also mit Bedacht.
Wie üblich
$(echo $'\n\n\n\n')
schluckt alle Zeilenvorschübe , nicht nur die letzte. Dies ist eine POSIX-Anforderung. Wenn Sie die LFs unversehrt lassen möchten, fügen Sie der Ausgabe einfach ein nachfolgendes Zeichen hinzu und entfernen Sie es anschließend wie im folgenden Rezept (sehen Sie sich das nachfolgende Zeichen an, mit dem Siex
einen Softlink lesen können, der auf eine Datei zeigt, die auf a endet$'\n'
):target="$(readlink -e "$file")x" target="${target%x}"
Shell-Variablen können das Byte NUL (
$'\0'
) nicht tragen . Sie werden einfach ignoriert, wenn sie instdout
oder auftretenstderr
.Der angegebene Befehl wird in einer Sub-Subshell ausgeführt. Es hat also keinen Zugriff auf
$PPID
Shell-Variablen und kann diese auch nicht ändern. Sie könnencatch
eine Shell-Funktion verwenden, auch integrierte Funktionen, aber diese können Shell-Variablen nicht ändern (da dies nicht alles$( .. )
kann, was darin ausgeführt wird). Wenn Sie also eine Funktion in der aktuellen Shell ausführen und deren stderr / stdout abfangen müssen, müssen Sie dies wie gewohnt mittempfile
s tun . (Es gibt Möglichkeiten, dies so zu tun, dass beim Unterbrechen der Hülle normalerweise keine Trümmer zurückbleiben. Dies ist jedoch komplex und verdient eine eigene Antwort.)F: Bash-Version?
printf %q
)F: Das sieht immer noch so umständlich aus.
ksh
viel sauberer gemacht werden kann. Ich bin es jedoch nicht gewohnt undksh
überlasse es anderen, ein ähnliches, einfach wiederverwendbares Rezept zu erstellenksh
.F: Warum nicht
ksh
dann verwenden?bash
Lösung istF: Das Skript kann verbessert werden
F: Es gibt einen Tippfehler.
: catch STDOUT STDERR cmd args..
soll lesen# catch STDOUT STDERR cmd args..
:
wird angezeigt,bash -x
während Kommentare lautlos verschluckt werden. So können Sie sehen, wo sich der Parser befindet, wenn Sie einen Tippfehler in der Funktionsdefinition haben. Es ist ein alter Debugging-Trick. Aber Vorsicht, Sie können leicht einige nette Nebeneffekte innerhalb der Argumente von erzeugen:
.Bearbeiten: Es wurden einige weitere hinzugefügt
;
, um das Erstellen eines Einzeilers zu vereinfachencatch()
. Und Abschnitt hinzugefügt, wie es funktioniert.quelle
catch
für Befehle, die einen der Streams umleiten oder Pipings ausführen? Es mag fraglich erscheinen, zu versuchen, beide Ausgaben zu erfassen, wenn eine davon leer ist (da der Befehl sie trotzdem umleitet). Es macht es jedoch einfacher, dasselbe Muster mit jedem Befehl immer wieder zu verwenden (insbesondere wenn der Befehl extern bereitgestellt wird und Sie nicht wissen, ob er umleitet), selbst wenn in einigen Fällen eine der Variablen dazu verdammt ist, leer zu sein.function echo_to_file { echo -n "$1" >"$2" ; }
und verwenden Sie sie danncatch
mit dieser Funktion. Funktioniert wie erwartet. Trotzdem wäre es schön, es ancatch
sich zu haben . (Ein ähnlicher "Trick" kann ausgeführt werden, um Pipes im Befehl zu haben.)catch
hier nicht! Die Erfassung einzelner Variablen ist direkt in die Shell integriert: stdout + stderr :var="$(command 2>&1)"; echo "command gives $? and outputs '$var'"
; catch stderr und leite stdout um:var="$(command 2>&1 >FILE)"
(nicht>FILE 2>&1
, dies würde stderr umleitenFILE
!); stdout-only :var="$(command)"; echo "command gives $? and has stdout '$var'"
, und fürstderr
oder andere FDs sehen eine andere Antwortcatch
Sollte in Funktion nicht die endgültige printf-Anweisung seinprintf 'return %q\n' "$ret" >&2
? Man möchte, dass die Funktioncatch
dencmd
Exit-Code zurückgibt, anstatt das Programm zu beenden.Dieser Befehl legt sowohl die Werte stdout (stdval) als auch stderr (errval) in der aktuell ausgeführten Shell fest:
eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"
vorausgesetzt, diese Funktion wurde definiert:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }
Ändern Sie execcommand in den erfassten Befehl, sei es "ls", "cp", "df" usw.
All dies basiert auf der Idee, dass wir alle erfassten Werte mit Hilfe der Funktion setval in eine Textzeile konvertieren könnten. Dann wird setval verwendet, um jeden Wert in dieser Struktur zu erfassen:
Konvertieren Sie jeden Erfassungswert in einen Setval-Aufruf:
Wickeln Sie alles in einen Ausführungsaufruf ein und geben Sie ihn wieder:
echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"
Sie erhalten die Deklarationsaufrufe, die jedes Setval erstellt:
declare -- stdval="I'm std" declare -- errval="I'm err"
Verwenden Sie eval, um diesen Code auszuführen (und die Variablen festzulegen):
eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"
und schließlich die eingestellten Vars wiedergeben:
echo "std out is : |$stdval| std err is : |$errval|
Es ist auch möglich, den Rückgabewert (Exit) einzuschließen.
Ein vollständiges Bash-Skript-Beispiel sieht folgendermaßen aus:
#!/bin/bash -- # The only function to declare: function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; } # a dummy function with some example values: function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; } # Running a command to capture all values # change execcommand to dummy or any other command to test. eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )" echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"
quelle
declare
keine atomaren Schreibvorgänge ausgeführt werden, wenn die gesamte Ausgabe länger als 1008 Byte ist (Ubuntu 16.04, Bash 4.3.46 (1)). Es gibt eine implizite Synchronisation zwischen den beidensetval
Aufrufen für stdout und stderr (diecat
insetval
for stderr kann nicht beendet werden, bevorsetval
for for stdout stderr geschlossen hat). Es gibt jedoch keine Synchronisation dersetval retval
, daher kann es irgendwo dazwischen kommen. In diesem Fallretval
wird in einer der beiden anderen Variablen verschluckt. Derretval
Fall läuft also nicht zuverlässig.capturable(){...}
(setval wie geschrieben) undcapture(){ eval "$( $@ 2> >(capturable stderr) > >(capturable stdout); )"; test -z "$stderr" }
.capture make ... && echo "$stdout" || less <<<"$stderr"
Seiten stderr oder druckt stdout, wenn es keine gibt. Funktioniert das für Sie oder hilft es Ihnen, wenn es funktioniert?Jonathan hat die Antwort . Als Referenz ist dies der ksh93-Trick. (erfordert eine nicht alte Version).
function out { echo stdout echo stderr >&2 } x=${ { y=$(out); } 2>&1; } typeset -p x y # Show the values
produziert
Die
${ cmds;}
Syntax ist nur eine Befehlsersetzung, die keine Unterschale erstellt. Die Befehle werden in der aktuellen Shell-Umgebung ausgeführt. Der Platz am Anfang ist wichtig ({
ist ein reserviertes Wort).Stderr der inneren Befehlsgruppe wird zu stdout umgeleitet (so dass es für die innere Ersetzung gilt). Als nächstes wird das stdout von
out
zugewieseny
und das umgeleitete stderr wird von erfasstx
, ohne dass diey
Subshell einer Befehlssubstitution normalerweise verloren geht.In anderen Shells ist dies nicht möglich, da für alle Konstrukte, die die Ausgabe erfassen, der Produzent in eine Subshell eingefügt werden muss, die in diesem Fall die Zuweisung enthalten würde.
Update: Jetzt auch von mksh unterstützt.
quelle
${ ... }
es sich nicht um eine Unterschale handelt, die den Rest leicht erklärbar macht. Ordentlicher Trick, solange Sie einenksh
verwenden müssen.Technisch gesehen sind Named Pipes keine temporären Dateien, und hier werden sie von niemandem erwähnt. Sie speichern nichts im Dateisystem und Sie können sie löschen, sobald Sie sie verbinden (damit Sie sie nie sehen):
#!/bin/bash -e foo () { echo stdout1 echo stderr1 >&2 sleep 1 echo stdout2 echo stderr2 >&2 } rm -f stdout stderr mkfifo stdout stderr foo >stdout 2>stderr & # blocks until reader is connected exec {fdout}<stdout {fderr}<stderr # unblocks `foo &` rm stdout stderr # filesystem objects are no longer needed stdout=$(cat <&$fdout) stderr=$(cat <&$fderr) echo $stdout echo $stderr exec {fdout}<&- {fderr}<&- # free file descriptors, optional
Auf diese Weise können Sie mehrere Hintergrundprozesse ausführen und deren Standard- und Standardwerte zu einem geeigneten Zeitpunkt usw. asynchron erfassen.
Wenn Sie dies nur für einen Prozess benötigen, können Sie genauso gut fest codierte fd-Nummern wie 3 und 4 anstelle der
{fdout}/{fderr}
Syntax verwenden (die ein freies fd für Sie findet).quelle
Ich denke, bevor man sagt "man kann nicht", sollten die Leute es zumindest mit ihren eigenen Händen versuchen ...
Einfache und saubere Lösung, ohne Verwendung
eval
oder etwas Exotisches1. Eine minimale Version
{ IFS=$'\n' read -r -d '' CAPTURED_STDERR; IFS=$'\n' read -r -d '' CAPTURED_STDOUT; } < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)
Benötigt:
printf
,read
2. Ein einfacher Test
Ein Dummy-Skript zum Produzieren
stdout
undstderr
:useless.sh
#!/bin/bash # # useless.sh # echo "This is stderr" 1>&2 echo "This is stdout"
Das eigentliche Skript, das erfasst
stdout
undstderr
:capture.sh
#!/bin/bash # # capture.sh # { IFS=$'\n' read -r -d '' CAPTURED_STDERR; IFS=$'\n' read -r -d '' CAPTURED_STDOUT; } < <((printf '\0%s\0' "$(./useless.sh)" 1>&2) 2>&1) echo 'Here is the captured stdout:' echo "${CAPTURED_STDOUT}" echo echo 'And here is the captured stderr:' echo "${CAPTURED_STDERR}" echo
Ausgabe von
capture.sh
3. Wie es funktioniert
Der Befehl
(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1
sendet die Standardausgabe von
some_command
toprintf '\0%s\0'
und erstellt so die Zeichenfolge\0${stdout}\n\0
(wobei\0
es sich um einNUL
Byte und\n
ein neues Zeilenzeichen handelt); Die Zeichenfolge\0${stdout}\n\0
wird dann zu dem Standardfehler umgeleitet, bei dem der Standardfehler vonsome_command
bereits vorhanden war, wodurch die Zeichenfolge erstellt wird${stderr}\n\0${stdout}\n\0
, die dann zurück zur Standardausgabe umgeleitet wird.Danach der Befehl
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
beginnt mit dem Lesen der Zeichenfolge
${stderr}\n\0${stdout}\n\0
bis zum erstenNUL
Byte und speichert den Inhalt in${CAPTURED_STDERR}
. Dann der BefehlIFS=$'\n' read -r -d '' CAPTURED_STDOUT;
liest die gleiche Zeichenfolge bis zum nächsten
NUL
Byte weiter und speichert den Inhalt in${CAPTURED_STDOUT}
.4. Machen Sie es unzerbrechlich
Die obige Lösung basiert auf einem
NUL
Byte für das Trennzeichen zwischenstderr
undstdout
, daher funktioniert es nicht, wenn aus irgendeinem Grundstderr
andereNUL
Bytes enthalten sind.Obwohl dies niemals passieren sollte, ist es möglich, das Skript vollständig unzerbrechlich zu machen, indem alle möglichen
NUL
Bytes vonstdout
undstderr
vor der Übergabe beider Ausgaben anread
(Desinfektion) entfernt werden -NUL
Bytes würden ohnehin verloren gehen, da es nicht möglich ist, sie in Shell-Variablen zu speichern :{ IFS=$'\n' read -r -d '' CAPTURED_STDOUT; IFS=$'\n' read -r -d '' CAPTURED_STDERR; } < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1)
Benötigt:
printf
,read
,tr
BEARBEITEN
Ich habe ein weiteres Beispiel für die Weitergabe des Exit-Status an die aktuelle Shell entfernt, da es, wie Andy in den Kommentaren ausgeführt hat, nicht so "unzerbrechlich" war, wie es sein sollte (da es nicht
printf
zum Puffern eines von verwendet wurde die Ströme). Für die Aufzeichnung füge ich den problematischen Code hier ein:Andy hat dann den folgenden "Änderungsvorschlag" zur Erfassung des Exit-Codes eingereicht:
Seine Lösung scheint zu funktionieren, hat aber das kleine Problem, dass der Exit-Status als letztes Fragment der Zeichenfolge platziert werden sollte, damit wir
exit "${CAPTURED_EXIT}"
in runden Klammern starten und den globalen Bereich nicht verschmutzen können, wie ich es versucht hatte das entfernte Beispiel. Das andere Problem ist, dass wir , da die Ausgabe seines Innerstenprintf
sofort an dasstderr
von angehängt wird,some_command
möglicheNUL
Bytes nicht mehr bereinigen könnenstderr
, da unter diesen jetzt auch unserNUL
Begrenzer ist.Nachdem ich ein wenig über den ultimativen Ansatz nachgedacht habe, habe ich eine Lösung gefunden, bei
printf
der sowohlstdout
der Exit-Code als auch der Exit-Code als zwei verschiedene Argumente zwischengespeichert werden, damit sie niemals stören.Das erste, was ich tat, war eine Möglichkeit, den Exit-Status dem dritten Argument von mitzuteilen
printf
, und dies war in seiner einfachsten Form (dh ohne Desinfektion) sehr einfach zu tun.5. Beibehaltung des Exit-Status - der Blaupause (ohne Desinfektion)
{ IFS=$'\n' read -r -d '' CAPTURED_STDERR; IFS=$'\n' read -r -d '' CAPTURED_STDOUT; (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_}); } < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)
Benötigt:
exit
,printf
,read
Die Dinge werden jedoch sehr chaotisch, wenn wir versuchen, Desinfektion einzuführen. Das Starten
tr
zur Bereinigung der Streams überschreibt tatsächlich unseren vorherigen Exit-Status. Daher besteht die einzige Lösung anscheinend darin, diesen in einen separaten Deskriptor umzuleiten, bevor er verloren geht, ihn dort zu belassen, bis ertr
seine Aufgabe zweimal erledigt, und ihn dann wieder an seinen Platz umzuleiten .Nach einigen ziemlich akrobatischen Umleitungen zwischen Dateideskriptoren kam ich zu diesem Ergebnis.
6. Beibehaltung des Ausgangsstatus durch Desinfektion - unzerbrechlich (umgeschrieben)
Der folgende Code ist eine Umschreibung des Beispiels, das ich entfernt habe. Außerdem werden mögliche
NUL
Bytes in den Streams bereinigt , sodassread
dies immer ordnungsgemäß funktioniert.{ IFS=$'\n' read -r -d '' CAPTURED_STDOUT; IFS=$'\n' read -r -d '' CAPTURED_STDERR; (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_}); } < <((printf '\0%s\0%d\0' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
Benötigt:
exit
,printf
,read
,tr
Diese Lösung ist wirklich robust. Der Exit-Code wird immer in einem anderen Deskriptor getrennt gehalten, bis er
printf
direkt als separates Argument erreicht wird.7. Die ultimative Lösung - eine Allzweckfunktion mit Exit-Status
Wir können den obigen Code auch in eine Allzweckfunktion umwandeln.
# SYNTAX: # catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND catch() { { IFS=$'\n' read -r -d '' "${1}"; IFS=$'\n' read -r -d '' "${2}"; (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_}); } < <((printf '\0%s\0%d\0' "$(((({ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) }
Benötigt:
cat
,exit
,printf
,read
,tr
Mit der
catch
Funktion können wir das folgende Snippet starten:catch MY_STDOUT MY_STDERR './useless.sh' echo "The \`./useless.sh\` program exited with code ${?}" echo echo 'Here is the captured stdout:' echo "${MY_STDOUT}" echo echo 'And here is the captured stderr:' echo "${MY_STDERR}" echo
und erhalten Sie das folgende Ergebnis:
8. Was passiert in den letzten Beispielen?
Hier folgt eine schnelle Schematisierung:
some_command
wird gestartet: Wir haben dannsome_command
'sstdout
auf dem Deskriptor 1,some_command
' sstderr
auf dem Deskriptor 2 undsome_command
's Exit-Code, der auf den Deskriptor 3 umgeleitet wirdstdout
wird antr
(Desinfektion) weitergeleitetstderr
wird mitstdout
(vorübergehend unter Verwendung des Deskriptors 4) ausgetauscht und antr
(Desinfektion) weitergeleitetstderr
(jetzt Deskriptor 1) ausgetauscht und an weitergeleitetexit $(cat)
stderr
(jetzt Deskriptor 3) wird zum Deskriptor 1 umgeleitet, Ende erweitert als zweites Argument vonprintf
exit $(cat)
wird durch das dritte Argument von erfasstprintf
printf
wird an den Deskriptor 2 umgeleitet, wostdout
bereits vorhanden warstdout
und Ausgabe vonprintf
wird an weitergeleitetread
9. Die POSIX-kompatible Version 1 (zerbrechlich)
Prozesssubstitutionen (die
< <()
Syntax) sind kein POSIX-Standard (obwohl dies de facto der Fall ist). In einer Shell, die die< <()
Syntax nicht unterstützt , können Sie das gleiche Ergebnis nur über die<<EOF … EOF
Syntax erzielen . Leider können wir keineNUL
Bytes als Trennzeichen verwenden, da diese vor dem Erreichen automatisch entfernt werdenread
. Wir müssen ein anderes Trennzeichen verwenden. Die natürliche Wahl fällt auf dasCTRL+Z
Zeichen (ASCII-Zeichen Nr. 26). Hier ist eine zerbrechliche Version (Ausgaben dürfen niemals dasCTRL+Z
Zeichen enthalten , sonst werden sie gemischt)._CTRL_Z_=$'\cZ' { IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDERR; IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDOUT; (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; exit ${_ERRNO_}); } <<EOF $((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(some_command)" "${?}" 1>&2) 2>&1) EOF
Benötigt:
exit
,printf
,read
10. Die POSIX-kompatible Version 2 (unzerbrechlich, aber nicht so gut wie die Nicht-POSIX-Version)
Und hier ist seine unzerbrechliche Version, direkt in Funktionsform (wenn eines
stdout
oderstderr
mehrereCTRL+Z
Zeichen enthält , wird der Stream abgeschnitten, aber niemals mit einem anderen Deskriptor ausgetauscht)._CTRL_Z_=$'\cZ' # SYNTAX: # catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND catch_posix() { { IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${1}"; IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${2}"; (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_}); } <<EOF $((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ${3}; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) EOF }
Benötigt:
cat
,cut
,exit
,printf
,read
,tr
quelle
find /proc
als nicht root. Die vorherigen Versionen funktionieren wunderbar, da Sie das Argument von printf verwenden, um stdout zu "puffern", um sicherzustellen, dass stdout erst gedruckt wird, nachdem der Befehl abgeschlossen und 100% von stderr gestreamt und gelöscht wurden. Die letzte Version verwendet jedoch nicht printf, um einen der Streams zu polieren, sondern nur den Exit-Code. Stderr und stdout sind verschachtelt, und stderr enthält nur den Wert eines Flushs. Wenn Sie das Problem beheben, wäre eine Erklärung sehr willkommen, da ich mich verliere, nachdem FD 4 eingeführt wurdecapture.sh
auf Ihrem Computer, nachdem Sie ihn mit der dritten Version gepatcht haben?stderr
, oder?exit "${CAPTURED_EXIT}"
in runden Klammern in der Lage sein möchten, den globalen Bereich nicht zu verschmutzen, wie ich es in meinem letzten Beispiel versucht habe . Das andere Problem ist, dass wir , da die Ausgabe Ihres Innerstenprintf
sofort an diestderr
von angehängt wird,some_command
möglicheNUL
Bytes nicht mehr bereinigen könnenstderr
, da sich unter diesen auch unserNUL
Trennzeichen befindet. Ich werde in den nächsten Tagen über etwas nachdenken.Die Auswertung hat mir nicht gefallen, daher hier eine Lösung, die einige Umleitungstricks verwendet, um die Programmausgabe in eine Variable zu erfassen und diese Variable dann zu analysieren, um die verschiedenen Komponenten zu extrahieren. Das Flag -w legt die Blockgröße fest und beeinflusst die Reihenfolge der Standard- / Fehlermeldungen im Zwischenformat. 1 bietet eine potenziell hohe Auflösung auf Kosten des Overheads.
####### # runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later. # limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output. # example: # var=$(keepBoth ls . notHere) # echo ls had the exit code "$(extractOne r "$var")" # echo ls had the stdErr of "$(extractOne e "$var")" # echo ls had the stdOut of "$(extractOne o "$var")" keepBoth() { ( prefix(){ ( set -o pipefail base64 -w 1 - | ( while read c do echo -E "$1" "$c" done ) ) } ( ( "$@" | prefix o >&3 echo ${PIPESTATUS[0]} | prefix r >&3 ) 2>&1 | prefix e >&1 ) 3>&1 ) } extractOne() { # extract echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode - }
quelle
Zum Nutzen des Lesers gibt es hier eine Lösung mit
tempfile
s.Die Frage war nicht
tempfile
s zu verwenden . Dies kann jedoch auf die unerwünschte Verschmutzung/tmp/
mit Tempfile zurückzuführen sein, falls die Schale stirbt. Beikill -9
einigentrap 'rm "$tmpfile1" "$tmpfile2"' 0
feuert nicht.Wenn Sie in einer Situation sind , wo Sie verwenden
tempfile
, wollen aber nie Trümmer hinter sich lassen , hier ist ein Rezept.Wieder wird es aufgerufen
catch()
(wie meine andere Antwort ) und hat die gleiche Aufrufsyntax:catch stdout stderr command args..
# Wrappers to avoid polluting the current shell's environment with variables : catch_read returncode FD variable catch_read() { eval "$3=\"\`cat <&$2\`\""; # You can use read instead to skip some fork()s. # However read stops at the first NUL byte, # also does no \n removal and needs bash 3 or above: #IFS='' read -ru$2 -d '' "$3"; return $1; } : catch_1 tempfile variable comand args.. catch_1() { { rm -f "$1"; "${@:3}" 66<&-; catch_read $? 66 "$2"; } 2>&1 >"$1" 66<"$1"; } : catch stdout stderr command args.. catch() { catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}"; }
Was es macht:
Es werden zwei
tempfile
s fürstdout
und erstelltstderr
. Diese werden jedoch fast sofort entfernt, so dass sie nur für eine sehr kurze Zeit verfügbar sind.catch_1()
fängtstdout
(FD 1) in eine Variable und bewegt sichstderr
zustdout
, so dass der nächste ("links")catch_1
diese fangen kann.Die Verarbeitung
catch
erfolgt von rechts nach links, sodass die linkecatch_1
zuletzt ausgeführt wird und abfängtstderr
.Das Schlimmste, was passieren kann, ist, dass einige temporäre Dateien angezeigt werden
/tmp/
, in diesem Fall jedoch immer leer sind. (Sie werden entfernt, bevor sie gefüllt werden.) Normalerweise sollte dies kein Problem sein, da tmpfs unter Linux ungefähr 128 KB Dateien pro GB Hauptspeicher unterstützt.Der angegebene Befehl kann auch auf alle lokalen Shell-Variablen zugreifen und diese ändern. Sie können also eine Shell-Funktion aufrufen, die Nebenwirkungen hat!
Dies gabelt sich nur zweimal für den
tempfile
Anruf.Bugs:
Fehlende gute Fehlerbehandlung für den Fall, dass dies
tempfile
fehlschlägt.Dies bewirkt das übliche
\n
Entfernen der Schale. Siehe Kommentar incatch_read()
.Sie können den Dateideskriptor nicht verwenden,
66
um Daten an Ihren Befehl weiterzuleiten. Wenn Sie dies benötigen, verwenden Sie einen anderen Deskriptor für die Umleitung, z. B.42
(Beachten Sie, dass sehr alte Shells nur FDs bis zu 9 anbieten).Dies kann keine NUL-Bytes (
$'\0'
) instdout
und verarbeitenstderr
. (NUL wird einfach ignoriert. Bei derread
Variante wird alles hinter einem NUL ignoriert.)Zu Ihrer Information:
quelle
Kurz gesagt, ich glaube, die Antwort lautet "Nein". Bei der Erfassung wird
$( ... )
nur die Standardausgabe für die Variable erfasst. Es gibt keine Möglichkeit, den Standardfehler in einer separaten Variablen zu erfassen. Also, was Sie haben, ist ungefähr so ordentlich wie es nur geht.quelle
eval
, scheint es tatsächlich möglich zu sein, aber als Sie es im Grunde darauf hinaus , eine bessere Shell oder Sprache zu verwenden. Es ist keine direkte Antwort auf die Frage, aber ich bin mit derselben Frage hierher gekommen und denke, dass ich langfristig zu einer Shell wechseln werde, die auf einer funktionalen Sprache wie Haskell basiert .Was ist mit ... = D.
GET_STDERR="" GET_STDOUT="" get_stderr_stdout() { GET_STDERR="" GET_STDOUT="" unset t_std t_err eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )" GET_STDERR=$t_err GET_STDOUT=$t_std } get_stderr_stdout "command" echo "$GET_STDERR" echo "$GET_STDOUT"
quelle
Wenn der Befehl 1) keine statusbehafteten Nebenwirkungen und 2) rechnerisch günstig ist, besteht die einfachste Lösung darin, ihn nur zweimal auszuführen. Ich habe dies hauptsächlich für Code verwendet, der während der Startsequenz ausgeführt wird, wenn Sie noch nicht wissen, ob die Festplatte funktionieren wird. In meinem Fall war es ein winziger
some_command
so dass es keinen Leistungseinbruch beim zweimaligen Ausführen gab und der Befehl keine Nebenwirkungen hatte.Der Hauptvorteil ist, dass dies sauber und leicht zu lesen ist. Die Lösungen hier sind ziemlich clever, aber ich würde es hassen, derjenige zu sein, der ein Skript mit den komplizierteren Lösungen pflegen muss. Ich würde den einfachen Run-it-zweimal-Ansatz empfehlen, wenn Ihr Szenario damit funktioniert, da es viel sauberer und einfacher zu warten ist.
Beispiel:
output=$(getopt -o '' -l test: -- "$@") errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null) if [[ -n "$errout" ]]; then echo "Option Error: $errout" fi
Auch dies ist nur in Ordnung, da getopt keine Nebenwirkungen hat. Ich weiß, dass es leistungssicher ist, da mein übergeordneter Code dies während des gesamten Programms weniger als 100 Mal aufruft und der Benutzer niemals 100 getopt-Aufrufe gegenüber 200 getopt-Aufrufen bemerkt.
quelle
out=$(some_command)
underr=$(some_command 2>&1 1>/dev/null)
?stdout
undstderr
die frei von Nebenwirkungen sind - selbst wenn ein Befehl unter normalen Umständen deterministisch ist , sind Fehler keine normalen Umstände. Dieser Ansatz wird wahrscheinlich auch anfällig für Rennbedingungen sein.Hier ist eine einfachere Variante, die nicht ganz dem entspricht, was das OP wollte, aber sich von allen anderen Optionen unterscheidet. Sie können alles bekommen, was Sie wollen, indem Sie die Dateideskriptoren neu anordnen.
Testbefehl:
%> cat xx.sh #!/bin/bash echo stdout >&2 echo stderr
was an sich tut:
Drucken Sie nun stdout, erfassen Sie stderr in einer Variablen und protokollieren Sie stdout in einer Datei
%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out") stdout %> cat out stdout %> echo $err stderr
Oder protokollieren Sie stdout und erfassen Sie stderr in einer Variablen:
export err=$(./xx.sh 3>&1 1>out 2>&3 ) %> cat out stdout %> echo $err stderr
Du hast die Idee.
quelle
Eine Problemumgehung, die hackig, aber möglicherweise intuitiver als einige der Vorschläge auf dieser Seite ist, besteht darin, die Ausgabestreams zu kennzeichnen, zusammenzuführen und anschließend anhand der Tags aufzuteilen. Zum Beispiel könnten wir stdout mit einem "STDOUT" -Präfix versehen:
function someCmd { echo "I am stdout" echo "I am stderr" 1>&2 } ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1) OUT=$(echo "$ALL" | grep "^STDOUT" | sed -e 's/^STDOUT//g') ERR=$(echo "$ALL" | grep -v "^STDOUT")
`` `
Wenn Sie wissen, dass stdout und / oder stderr eine eingeschränkte Form haben, können Sie ein Tag erstellen, das nicht mit dem zulässigen Inhalt in Konflikt steht.
quelle
sed
Besteht dieses Risiko, das das Ergebnis von interpretiertsomeCmd
? Mögliche unerwünschte Codeausführung?sed
interpretiert nur die Zeichenfolgenargumente, dhs/^/STDOUT/g
unds/^STDOUT//g
. Da es sich um feste, bekannte Zeichenfolgen handelt, gibt es keinen Injektions- / unerwünschten Ausführungsvektor. Der Standard und der Standard dessomeCmd
Willens fließen durch den Standard und den Standard vonsed
; Sie werden bearbeitet, aber nicht ausgeführt. Ebenso für die Anrufe angrep
.someCmd
niemals eine Zeile enthalten, die mit dem Text "sentinel" beginntSTDOUT
. Wenn dies nicht zutrifft, könnten wir einen anderen Sentinel auswählen. Wenn die Ausgabe jedoch willkürlich ist (z. B. benutzerdefiniert), kann diese Methode nicht verwendet werden, da kein Sentinel-Text von den Daten unterschieden werden kann.WARNUNG: NICHT (noch?) ARBEITEN!
Das Folgende scheint ein möglicher Grund dafür zu sein, dass es funktioniert, ohne temporäre Dateien zu erstellen, und auch nur unter POSIX sh. es erfordert jedoch base64 und ist aufgrund der Codierung / Decodierung möglicherweise nicht so effizient und verwendet auch "größeren" Speicher.
Das Hauptproblem ist jedoch, dass alles rassig erscheint. Versuchen Sie es mit einer Exe wie:
exe () {cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic> & 2}
und Sie werden sehen, dass sich z. B. Teile der Base64-codierten Zeile oben in der Datei befinden, Teile am Ende und das nicht decodierte Stderr-Material in der Mitte.
Nun, selbst wenn die folgende Idee nicht zum Funktionieren gebracht werden kann (was ich annehme), kann sie als Anti-Beispiel für Leute dienen, die fälschlicherweise glauben, dass sie so funktionieren könnte.
Idee (oder Anti-Beispiel):
#!/bin/sh exe() { echo out1 echo err1 >&2 echo out2 echo out3 echo err2 >&2 echo out4 echo err3 >&2 echo -n err4 >&2 } r="$( { exe | base64 -w 0 ; } 2>&1 )" echo RAW printf '%s' "$r" echo RAW o="$( printf '%s' "$r" | tail -n 1 | base64 -d )" e="$( printf '%s' "$r" | head -n -1 )" unset r echo echo OUT printf '%s' "$o" echo OUT echo echo ERR printf '%s' "$e" echo ERR
gibt (mit dem stderr-newline fix):
(Zumindest auf Debians Schlag und Schlag)
quelle
Hier ist eine Variante der @ madmurphy-Lösung, die für beliebig große stdout / stderr-Streams funktionieren, den Exit-Rückgabewert beibehalten und Nullen im Stream verarbeiten soll (indem sie in Zeilenumbrüche konvertiert werden).
function buffer_plus_null() { local buf IFS= read -r -d '' buf || : echo -n "${buf}" printf '\0' } { IFS= time read -r -d '' CAPTURED_STDOUT; IFS= time read -r -d '' CAPTURED_STDERR; (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}"); } < <((({ { some_command ; echo "${?}" 1>&3; } | tr '\0' '\n' | buffer_plus_null; } 2>&1 1>&4 | tr '\0' '\n' | buffer_plus_null 1>&4 ) 3>&1 | xargs printf '%s\0' 1>&4) 4>&1 )
Nachteile:
read
Befehle sind der teuerste Teil der Operation. Beispiel:find /proc
Auf einem Computer mit 500 Prozessen dauert es 20 Sekunden (während der Befehl nur 0,5 Sekunden betrug). Das erste Mal dauert das Lesen 10 Sekunden und das zweite Mal 10 Sekunden länger, wodurch sich die Gesamtzeit verdoppelt.Erklärung des Puffers
Die ursprüngliche Lösung bestand in einem Argument zum
printf
Puffern des Streams. Da jedoch der Exit-Code als letztes verwendet werden muss, besteht eine Lösung darin, sowohl stdout als auch stderr zu puffern. Ich habe es versucht,xargs -0 printf
aber dann haben Sie schnell angefangen, "maximale Argumentlängenbeschränkungen" zu erreichen. Also entschied ich mich für eine Lösung, eine schnelle Pufferfunktion zu schreiben:read
diese Option , um den Stream in einer Variablen zu speichernread
wird beendet, wenn der Stream endet oder eine Null empfangen wird. Da wir die Nullen bereits entfernt haben, endet sie, wenn der Stream geschlossen wird, und gibt einen Wert ungleich Null zurück. Da dies erwartetes Verhalten ist, fügen wir die|| :
Bedeutung "oder wahr" hinzu, so dass die Zeile immer als wahr (0) ausgewertet wird.echo -n "${buf}"
ist ein eingebauter Befehl und daher nicht auf die Argumentlängenbeschränkung beschränktquelle