Erfassen Sie stdout und stderr in verschiedenen Variablen


75

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 outund stderr in zu bekommen , aber ich möchte die temporäre Datei vermeiden.errsome_command

error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< error_file)
rm $error_file

Diese Frage enthält meine Frage, beantwortet sie aber nicht.
Ntc2

3
fwiw, es ist in ksh93 möglich. ksh -c 'function f { echo out; echo err >&2; }; x=${ { y=$(f); } 2>&1;}; typeset -p x y'
Ormaaj

1
@ormaaj: Kannst du erklären, wie die ksh93-Technik funktioniert? Möglicherweise müssen Sie eine Antwort geben.
Jonathan Leffler


3
@gniourf_gniourf Nun, diese Frage ist hinter dieser zurück. Wenn es also ein Duplikat gibt, sollte es diese sein: Erfassen Sie sowohl stdout als auch stderr in bash :-D

Antworten:


43

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_stdzeilenweise 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) .


2
Deshalb würde ich die Rücksendung genauso behandeln. Versuchen Sieeval "$( 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
TheConstructor

1
Danke für das Konzept. Ich habe es hier ein wenig erweitert (destilliert): stackoverflow.com/a/28796214/2350426

1
typeset -p t_outund typeset -p t_errkönnte gemischt sein, wodurch die Ausgabe unbrauchbar wird.
4ae1e1

1
@ 4ae1e1 Ich habe über diese Möglichkeit nachgedacht, konnte aber nicht bestätigen, dass dies passieren kann.
TheConstructor

1
@ TheConstructor Hmm, ich denke du hast recht. Ich war in Zsh und benutzte >>()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 -pist definitiv nicht atomar, oder?
4ae1e1

19

Dies dient zum Abfangen von stdout und stderr in verschiedenen Variablen. Wenn Sie nur fangen wollen stderr, stdoutwie es ist, gibt es eine bessere und kürzere Lösung .

Um es zusammenzufassen alles bis zum Nutzen des Lesers, hier ist ein

Einfache wiederverwendbare bashLösung

Diese Version verwendet Subshells und läuft ohne tempfiles. (Eine tempfileVersion, 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 VAR2vor jeden stellen command args..und fertig.

Einige if cmd args..; thenwerden werden if 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 verwendet eval, um die beiden Variablen zu setzen. Dies ähnelt https://stackoverflow.com/a/18086548

Betrachten 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 aus dummy 1 2a 3bund speichert es stdoutin __1für die spätere Verwendung. So __1wird 2a. Es leitet auch stderrvon dummyzu um stdout, so dass sich der äußere Fang sammeln kannstdout

  • ret=$?; fängt den Exit-Code ab, der ist 1

  • printf '%q=%q\n' "$1" "$__1" >&2;gibt dann out=2aan aus stderr. stderrhier verwendet wird, da der Strom stdoutbereits über die Rolle übernommen hat stderrden dummyBefehls.

  • exit $retLeiten Sie dann den Exit-Code ( 1) an die nächste Stufe weiter.

Nun zum Äußeren __2="$( ... )":

  • Dies fängt stdoutdas Obige, das stderrder dummyAufruf ist, in eine Variable auf __2. (Wir könnten hier wiederverwenden __1, aber ich habe __2es früher weniger verwirrend gemacht.) So __2wird3b

  • ret="$?";fängt den (zurückgegebenen) Rückkehrcode 1(von dummy) erneut ab

  • printf '%s=%q\n' "$2" "$__2" >&2;gibt dann err=3aan aus stderr. stderrwird erneut verwendet, da es bereits zur Ausgabe der anderen Variablen verwendet wurde out=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 kann catch.

Bitte beachten Sie, dass wir als Optimierung diese 2 auch printfals einzelne wie printf '%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 outaus ist $1, 2aist aus stdoutdem dummy, erraus $2, 3bist von stderrden dummy, und das 1ist von dem Rückkehrcode dummy.

Bitte beachten Sie, dass %qim Format von printffür das Zitieren gesorgt wird, sodass die Shell die richtigen (einzelnen) Argumente sieht, wenn es darum geht eval. 2aund 3bsind 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 die 2>&1) abfangen und sie mit in die aktuelle Shell analysieren eval.

Auf diese Weise werden die 2 Variablen und auch der Rückkehrcode gesetzt.

F: Es verwendet, evalwas böse ist. Ist es also sicher?

  • Solange printf %qes 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 Sie xeinen 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 in stdoutoder auftreten stderr.

  • Der angegebene Befehl wird in einer Sub-Subshell ausgeführt. Es hat also keinen Zugriff auf $PPIDShell-Variablen und kann diese auch nicht ändern. Sie können catcheine 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 mit tempfiles 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?

  • Ich denke du brauchst Bash 4 und höher (wegen printf %q)

F: Das sieht immer noch so umständlich aus.

  • Richtig. Eine andere Antwort hier zeigt, wie es kshviel sauberer gemacht werden kann. Ich bin es jedoch nicht gewohnt und kshüberlasse es anderen, ein ähnliches, einfach wiederverwendbares Rezept zu erstellen ksh.

F: Warum nicht kshdann verwenden?

  • Weil dies eine bashLösung ist

F: Das Skript kann verbessert werden

  • Natürlich können Sie einige Bytes herausdrücken und kleinere oder unverständlichere Lösungen erstellen. TU es einfach ;)

F: Es gibt einen Tippfehler. : catch STDOUT STDERR cmd args..soll lesen# catch STDOUT STDERR cmd args..

  • Eigentlich ist das so gemeint. :wird angezeigt, bash -xwä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 vereinfachen catch(). Und Abschnitt hinzugefügt, wie es funktioniert.


Dies ist eine sehr interessante Lösung, da sie die Verwendung erheblich erleichtert. Sie sollten jedoch ein wenig mehr Details zur Funktionsweise angeben, da es nicht dem allgemeinen Muster anderer Lösungsvorschläge entspricht.
Jwatkins

Wie wäre es damit catchfü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.
Adam Badura

Bisher habe ich eine einfache Problemumgehung gefunden. Definieren Sie einfach eine einfache Funktion wie function echo_to_file { echo -n "$1" >"$2" ; }und verwenden Sie sie dann catchmit dieser Funktion. Funktioniert wie erwartet. Trotzdem wäre es schön, es an catchsich zu haben . (Ein ähnlicher "Trick" kann ausgeführt werden, um Pipes im Befehl zu haben.)
Adam Badura

@AdamBadura die Frage war, 2 verschiedene Variablen parallel zu fangen. Wenn Sie nur eine einzige Variable erfassen möchten, benötigen Sie diese catchhier 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 umleiten FILE!); stdout-only : var="$(command)"; echo "command gives $? and has stdout '$var'", und für stderroder andere FDs sehen eine andere Antwort
Tino

catchSollte in Funktion nicht die endgültige printf-Anweisung sein printf 'return %q\n' "$ret" >&2? Man möchte, dass die Funktion catchden cmdExit-Code zurückgibt, anstatt das Programm zu beenden.
Jim Fischer

15

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:

execcommand 2> CaptureErr > CaptureOut

Konvertieren Sie jeden Erfassungswert in einen Setval-Aufruf:

execcommand 2> >(setval errval) > >(setval stdval)

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|"

1
Es gibt eine Race-Bedingung, da declarekeine 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 beiden setvalAufrufen für stdout und stderr (die catin setvalfor stderr kann nicht beendet werden, bevor setvalfor for stdout stderr geschlossen hat). Es gibt jedoch keine Synchronisation der setval retval, daher kann es irgendwo dazwischen kommen. In diesem Fall retvalwird in einer der beiden anderen Variablen verschluckt. Der retvalFall läuft also nicht zuverlässig.
Tino

Ich denke, ich mag diesen Ansatz ... irgendwie. Gibt es eine Möglichkeit, diese Auswertung in eine separate Funktion zu verschieben und den Befehl an diese zu übergeben? Wenn ich das versuche, deklariert es weder errval noch stdval.
Justin

Ich habe capturable(){...}(setval wie geschrieben) und capture(){ 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?
John P

14

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

x=stderr
y=stdout

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 outzugewiesen yund das umgeleitete stderr wird von erfasst x, ohne dass die ySubshell 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.


2
Vielen Dank. Der entscheidende Punkt ist, dass ${ ... }es sich nicht um eine Unterschale handelt, die den Rest leicht erklärbar macht. Ordentlicher Trick, solange Sie einen kshverwenden müssen.
Jonathan Leffler

9
Dies ist keine Antwort auf die Frage. Die Frage bezieht sich auf die Bash, während Ihre Antwort auf ksh gültig ist.
Mshamma

1
@mshamma Offensichtlich. Lesen Sie den letzten Absatz.
Ormaaj

12

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).


8

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 evaloder 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 stdoutund stderr:useless.sh

#!/bin/bash
#
# useless.sh
#

echo "This is stderr" 1>&2
echo "This is stdout" 

Das eigentliche Skript, das erfasst stdoutund stderr: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

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_commandto printf '\0%s\0'und erstellt so die Zeichenfolge \0${stdout}\n\0(wobei \0es sich um ein NULByte und \nein neues Zeilenzeichen handelt); Die Zeichenfolge \0${stdout}\n\0wird dann zu dem Standardfehler umgeleitet, bei dem der Standardfehler von some_commandbereits 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\0bis zum ersten NULByte 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 NULByte weiter und speichert den Inhalt in ${CAPTURED_STDOUT}.

4. Machen Sie es unzerbrechlich

Die obige Lösung basiert auf einem NULByte für das Trennzeichen zwischen stderrund stdout, daher funktioniert es nicht, wenn aus irgendeinem Grund stderrandere NULBytes enthalten sind.

Obwohl dies niemals passieren sollte, ist es möglich, das Skript vollständig unzerbrechlich zu machen, indem alle möglichen NULBytes von stdoutund stderrvor der Übergabe beider Ausgaben an read(Desinfektion) entfernt werden - NULBytes 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 printfzum 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_commandan 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 stderreine dritte Information hinzufügen , eine weitere NULplus den exitStatus des Befehls. Es wird nach, stderraber 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 printfsofort an das stderrvon angehängt wird, some_commandmögliche NULBytes 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 printfder 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 trzur 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 trseine 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 NULBytes in den Streams bereinigt , sodass readdies 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 printfdirekt 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 catchFunktion 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:

  1. some_commandwird gestartet: Wir haben dann some_command's stdoutauf dem Deskriptor 1, some_command' s stderrauf dem Deskriptor 2 und some_command's Exit-Code, der auf den Deskriptor 3 umgeleitet wird
  2. stdoutwird an tr(Desinfektion) weitergeleitet
  3. stderrwird mit stdout(vorübergehend unter Verwendung des Deskriptors 4) ausgetauscht und an tr(Desinfektion) weitergeleitet
  4. Der Exit-Code (Deskriptor 3) wird gegen stderr(jetzt Deskriptor 1) ausgetauscht und an weitergeleitetexit $(cat)
  5. stderr (jetzt Deskriptor 3) wird zum Deskriptor 1 umgeleitet, Ende erweitert als zweites Argument von printf
  6. Der Exit-Code von exit $(cat)wird durch das dritte Argument von erfasstprintf
  7. Die Ausgabe von printfwird an den Deskriptor 2 umgeleitet, wo stdoutbereits vorhanden war
  8. Die Verkettung stdoutund Ausgabe von printfwird 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 … EOFSyntax erzielen . Leider können wir keine NULBytes 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+ZZeichen (ASCII-Zeichen Nr. 26). Hier ist eine zerbrechliche Version (Ausgaben dürfen niemals das CTRL+ZZeichen 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 stdoutoder stderrmehrere CTRL+ZZeichen 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


Das ist sehr klug.
Andy

Die letzte Version funktioniert nicht richtig. Versuchen Sie den Befehl find /procals 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 wurde
Andy

Hallo Andy! Vielen Dank für Ihre Kommentare und Bearbeitungsvorschläge. Was ist die Ausgabe capture.shauf Ihrem Computer, nachdem Sie ihn mit der dritten Version gepatcht haben?
Madmurphy

Ok, ich sehe es jetzt. Die letzte Version wird abgeschnitten stderr, oder?
Madmurphy

1
Guter Punkt. Ihre Lösung funktioniert, hat jedoch das kleine Problem, dass der Exit-Status das letzte Stück der Zeichenfolge darstellen sollte, wenn wir 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 Innersten printfsofort an die stderrvon angehängt wird, some_commandmögliche NULBytes nicht mehr bereinigen können stderr, da sich unter diesen auch unser NUL Trennzeichen befindet. Ich werde in den nächsten Tagen über etwas nachdenken.
Madmurphy

4

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 -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

3

Zum Nutzen des Lesers gibt es hier eine Lösung mit tempfiles.

Die Frage war nicht tempfiles zu verwenden . Dies kann jedoch auf die unerwünschte Verschmutzung /tmp/mit Tempfile zurückzuführen sein, falls die Schale stirbt. Bei kill -9einigen trap 'rm "$tmpfile1" "$tmpfile2"' 0feuert nicht.

Wenn Sie in einer Situation sind , wo Sie verwendentempfile , 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 tempfiles für stdoutund erstellt stderr. Diese werden jedoch fast sofort entfernt, so dass sie nur für eine sehr kurze Zeit verfügbar sind.

  • catch_1()fängt stdout(FD 1) in eine Variable und bewegt sich stderrzu stdout, so dass der nächste ("links") catch_1diese fangen kann.

  • Die Verarbeitung catcherfolgt von rechts nach links, sodass die linke catch_1zuletzt ausgeführt wird und abfängt stderr.

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 tempfileAnruf.

Bugs:

  • Fehlende gute Fehlerbehandlung für den Fall, dass dies tempfilefehlschlägt.

  • Dies bewirkt das übliche \nEntfernen der Schale. Siehe Kommentar in catch_read().

  • Sie können den Dateideskriptor nicht verwenden, 66um 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') in stdoutund verarbeiten stderr. (NUL wird einfach ignoriert. Bei der readVariante wird alles hinter einem NUL ignoriert.)

Zu Ihrer Information:

  • Mit Unix können wir auf gelöschte Dateien zugreifen, sofern Sie einen Verweis darauf haben (z. B. ein offenes Dateihandle). Auf diese Weise können wir sie öffnen und dann entfernen.

2

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.


1
@ormaaj: Wie aus den Antworten hervorgeht 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 .
James Haigh

2

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"

2
Dies scheint ein Wrapper um die erste Antwort zu sein , der keine neuen Funktionen hinzufügt. Wie ist das anders / nützlicher?
ntc2

0

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 winzigersome_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.


Könnten Sie ein Beispiel geben? Ich vermute so etwas wie out=$(some_command)und err=$(some_command 2>&1 1>/dev/null)?
Ntc2

@eicto - dann müssen Sie eine der oben genannten Lösungen verwenden - dies ist nur eine gute Lösung, wenn Ihr Befehl keine Nebenwirkungen hat und rechnerisch billig ist
Hamy

1
Ich bezweifle, dass es viele Anwendungsfälle gibt, die eine separate Behandlung erfordern stdoutund stderrdie 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.
James Haigh

0

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:

%> ./xx.sh
stdout
stderr

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.


0

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.


Haben Sie einen allgemeineren Weg, dies zu tun, der für alle Ausgaben funktioniert, siehe meine Antwort auf diese Frage.
Mncl

sedBesteht dieses Risiko, das das Ergebnis von interpretiert someCmd? Mögliche unerwünschte Codeausführung?
Adrelanos

@adrelanos AFAIK in den obigen Beispielen sedinterpretiert nur die Zeichenfolgenargumente, dh s/^/STDOUT/gund s/^STDOUT//g. Da es sich um feste, bekannte Zeichenfolgen handelt, gibt es keinen Injektions- / unerwünschten Ausführungsvektor. Der Standard und der Standard des someCmdWillens fließen durch den Standard und den Standard von sed; Sie werden bearbeitet, aber nicht ausgeführt. Ebenso für die Anrufe an grep.
Warbo

@adrelanos Beachten Sie, dass ich davon ausgehe, dass stdout und stderr von someCmdniemals eine Zeile enthalten, die mit dem Text "sentinel" beginnt STDOUT. 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.
Warbo

0

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.

  • Selbst im einfachen Fall würde es bereits fehlschlagen, wenn die letzte stderr-Zeile keine neue Zeile enthält. Dies kann zumindest in einigen Fällen behoben werden, indem exe durch "{exe; echo> & 2;}" ersetzt wird, dh eine neue Zeile hinzugefügt wird.
  • 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):

$ ./ggg 
RAW
err1
err2
err3
err4

b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW

OUT
out1
out2
out3
out4OUT

ERR
err1
err2
err3
err4ERR

(Zumindest auf Debians Schlag und Schlag)


0

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:

  • Die readBefehle sind der teuerste Teil der Operation. Beispiel: find /procAuf 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 printfPuffern 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 printfaber 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:

  1. Verwenden Sie readdiese Option , um den Stream in einer Variablen zu speichern
  2. Dies readwird 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.
  3. Jetzt, da ich weiß, dass der Stream beendet ist, kann ich ihn sicher wiedergeben.
  4. echo -n "${buf}" ist ein eingebauter Befehl und daher nicht auf die Argumentlängenbeschränkung beschränkt
  5. Zuletzt fügen Sie am Ende ein Nulltrennzeichen hinzu.
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.