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 Exotisches
1. 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
und stderr
:useless.sh
#!/bin/bash
echo "This is stderr" 1>&2
echo "This is stdout"
Das eigentliche Skript, das erfasst stdout
und stderr
:capture.sh
#!/bin/bash
{
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
Here is the captured stdout:
This is stdout
And here is the captured stderr:
This is stderr
3. Wie es funktioniert
Der Befehl
(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1
sendet die Standardausgabe von some_command
to printf '\0%s\0'
und erstellt so die Zeichenfolge \0${stdout}\n\0
(wobei \0
es sich um ein NUL
Byte und \n
ein neues Zeilenzeichen handelt); Die Zeichenfolge \0${stdout}\n\0
wird dann zu dem Standardfehler umgeleitet, bei dem der Standardfehler von some_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 ersten NUL
Byte und speichert den Inhalt in ${CAPTURED_STDERR}
. Dann der Befehl
IFS=$'\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 zwischen stderr
und stdout
, daher funktioniert es nicht, wenn aus irgendeinem Grund stderr
andere NUL
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 von stdout
und stderr
vor der Übergabe beider Ausgaben an read
(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:
Beibehaltung des Exit-Status (immer noch unzerbrechlich)
Die folgende Variante überträgt auch den Exit-Status von some_command
an die aktuelle Shell:
{
IFS= read -r -d '' CAPTURED_STDOUT;
IFS= read -r -d '' CAPTURED_STDERR;
(IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr -d '\0'; printf '\0'; } 2>&1- 1>&4- | tr -d '\0' 1>&4-) 3>&1- | xargs printf '\0%s\0' 1>&4-) 4>&1-)
Benötigt: printf
, read
, tr
,xargs
Andy hat dann den folgenden "Änderungsvorschlag" zur Erfassung des Exit-Codes eingereicht:
Einfache und saubere Lösung, die den Ausgangswert spart
Wir können am Ende stderr
eine dritte Information hinzufügen , eine weitere NUL
plus den exit
Status des Befehls. Es wird nach, stderr
aber vorher ausgegebenstdout
{
IFS= read -r -d '' CAPTURED_STDERR;
IFS= read -r -d '' CAPTURED_EXIT;
IFS= read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1)
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 Innersten printf
sofort an das stderr
von angehängt wird, some_command
mögliche NUL
Bytes nicht mehr bereinigen können stderr
, da unter diesen jetzt auch unser NUL
Begrenzer ist.
Nachdem ich ein wenig über den ultimativen Ansatz nachgedacht habe, habe ich eine Lösung gefunden, bei printf
der sowohl stdout
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 er tr
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 , sodass read
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.
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:
The `./useless.sh` program exited with code 0
Here is the captured stdout:
This is stderr 1
This is stderr 2
And here is the captured stderr:
This is stdout 1
This is stdout 2
8. Was passiert in den letzten Beispielen?
Hier folgt eine schnelle Schematisierung:
some_command
wird gestartet: Wir haben dann some_command
's stdout
auf dem Deskriptor 1, some_command
' s stderr
auf dem Deskriptor 2 und some_command
's Exit-Code, der auf den Deskriptor 3 umgeleitet wird
stdout
wird an tr
(Desinfektion) weitergeleitet
stderr
wird mit stdout
(vorübergehend unter Verwendung des Deskriptors 4) ausgetauscht und an tr
(Desinfektion) weitergeleitet
- Der Exit-Code (Deskriptor 3) wird gegen
stderr
(jetzt Deskriptor 1) ausgetauscht und an weitergeleitetexit $(cat)
stderr
(jetzt Deskriptor 3) wird zum Deskriptor 1 umgeleitet, Ende erweitert als zweites Argument von printf
- Der Exit-Code von
exit $(cat)
wird durch das dritte Argument von erfasstprintf
- Die Ausgabe von
printf
wird an den Deskriptor 2 umgeleitet, wo stdout
bereits vorhanden war
- Die Verkettung
stdout
und Ausgabe von printf
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 keine NUL
Bytes als Trennzeichen verwenden, da diese vor dem Erreichen automatisch entfernt werden read
. Wir müssen ein anderes Trennzeichen verwenden. Die natürliche Wahl fällt auf das CTRL+Z
Zeichen (ASCII-Zeichen Nr. 26). Hier ist eine zerbrechliche Version (Ausgaben dürfen niemals das CTRL+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
oder stderr
mehrere CTRL+Z
Zeichen enthält , wird der Stream abgeschnitten, aber niemals mit einem anderen Deskriptor ausgetauscht).
_CTRL_Z_=$'\cZ'
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