Cattura stdout e stderr in diverse variabili

È ansible memorizzare o acquisire stdout e stderr in variabili diverse , senza utilizzare un file temporaneo? In questo momento lo faccio per ottenere stdout in out e stderr in err quando some_command , ma mi piacerebbe evitare il file temporaneo.

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

Ok, è un po ‘brutto, ma ecco una soluzione:

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

dove (echo std; echo err >&2) deve essere sostituito dal comando effettivo. L’output di stdout viene salvato nell’array $t_std riga per riga omettendo le nuove (il -t ) e lo stderr in $t_err .

Se non ti piacciono gli array che puoi fare

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

che praticamente imita il comportamento di var=$(cmd) eccezione del valore di $? che ci porta all’ultima modifica:

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

Qui $? è conservato in $t_ret

Testato su Debian wheezy usando GNU bash , versione 4.2.37 (1) -release (i486-pc-linux-gnu) .

Jonathan ha la risposta . Per riferimento, questo è il trucco ksh93. (richiede una versione non antica).

 function out { echo stdout echo stderr >&2 } x=${ { y=$(out); } 2>&1; } typeset -pxy # Show the values 

produce

 x=stderr y=stdout 

La syntax ${ cmds;} è solo una sostituzione di comando che non crea una subshell. I comandi vengono eseguiti nell’ambiente di shell corrente. Lo spazio all’inizio è importante ( { è una parola riservata).

Lo stderr del gruppo di comandi interno viene reindirizzato allo stdout (in modo che si applichi alla sostituzione interna). Successivamente, lo stdout di out è assegnato a y , e lo stderr reindirizzato viene catturato da x , senza la solita perdita di y per la subshell di una sostituzione di comando.

Non è ansible in altre shell, perché tutti i costrutti che catturano l’output richiedono di mettere il produttore in una subshell, che in questo caso includerebbe l’assegnazione.

aggiornamento: ora supportato anche da mksh.

Questo comando imposta sia i valori stdout (stdval) che stderr (errval) nella shell corrente corrente:

 eval "$( execcommand 2> >(setval errval) > >(setval stdval); )" 

a condizione che questa funzione sia stata definita:

 function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; } 

Modifica il comando exec nel comando acquisito, che si tratti di “ls”, “cp”, “df”, ecc.


Tutto questo si basa sull’idea che potremmo convertire tutti i valori catturati in una riga di testo con l’aiuto della funzione setval, quindi setval viene usato per catturare ogni valore in questa struttura:

 execcommand 2> CaptureErr > CaptureOut 

Converti ogni valore di cattura in una chiamata setval:

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

Avvolgi tutto all’interno di una chiamata execute ed echo:

 echo "$( execcommand 2> >(setval errval) > >(setval stdval) )" 

Otterrai le chiamate dichiarate che ogni setval crea:

 declare -- stdval="I'm std" declare -- errval="I'm err" 

Per eseguire quel codice (e ottenere il set di vars) usa eval:

 eval "$( execcommand 2> >(setval errval) > >(setval stdval) )" 

e infine riecheggiano le vars dei set:

 echo "std out is : |$stdval| std err is : |$errval| 

È anche ansible includere il valore di ritorno (uscita).
Un esempio di script bash completo ha il seguente aspetto:

 #!/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|" 

Per riassumere tutto a beneficio del lettore, ecco un

Semplice soluzione di bash riutilizzabile

Questa versione utilizza subshells e funziona senza tempfile s. (Per una versione tempfile che gira senza subshell, vedi la mia altra risposta .)

 : 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 )"; } 

Esempio di utilizzo:

 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" 

questa stampa

 ret=3 stdout=$'\ndiffcult\n data ' stderr=$'\nother\n difficult \n data ' 

Quindi può essere usato senza pensarci più a fondo. Basta mettere catch VAR1 VAR2 davanti a qualsiasi command args.. e il gioco è fatto.

Alcuni if cmd args..; then if cmd args..; then diventerà if catch VAR1 VAR2 cmd args..; then if catch VAR1 VAR2 cmd args..; then . Davvero niente di complesso.

Discussione

D: Come funziona?

Semplicemente avvolge le idee dalle altre risposte qui in una funzione, in modo che possa essere facilmente riutilizzata.

catch() utilizza fondamentalmente eval per impostare le due variabili. Questo è simile a https://stackoverflow.com/a/18086548

Prendi in considerazione una chiamata di catch out err dummy 1 2a 3b :

  • saltiamo l’ eval "$({ e il __2="$( per ora. Verrò a questo più tardi.

  • __1="$("$("${@:3}")"; } 2>&1; esegue dummy 1 2 3 e memorizza lo stdout in __1 per un uso successivo. Quindi __1 diventa 2a . Reindirizza anche stderr del dummy a stdout , in modo tale che il fermo esterno possa raccogliere stdout

  • ret=$?; prende il codice di uscita, che è 1

  • printf '%q=%q\n' "$1" "$__1" >&2; quindi emette out=2a su stderr . stderr è usato qui, poiché lo stdout corrente ha già assunto il ruolo di stderr del comando dummy .

  • exit $ret quindi inoltra il codice di uscita ( 1 ) alla fase successiva.

Ora verso l’esterno __2="$( ... )" :

  • Questo cattura lo stdout di quanto sopra, che è lo stderr della chiamata dummy , nella variabile __2 . (Potremmo riutilizzare __1 qui, ma ho usato __2 per renderlo meno confuso.). Quindi __2 diventa 3b

  • ret="$?"; intercetta nuovamente il codice di ritorno (restituito) 1 (da dummy )

  • printf '%s=%q\n' "$2" "$__2" >&2; quindi invia err=3a a stderr . stderr è usato di nuovo, poiché è già stato usato per generare l’altra variabile out=2a .

  • printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to prendere.

Si noti che, come ottimizzazione, avremmo potuto scrivere quei 2 printf come uno solo come printf '%s=%q\n( exit %q ) “$ __ 2” “$ ret” `pure.

Quindi cosa abbiamo finora?

Abbiamo seguito per scrivere su stderr:

 out=2a err=3b ( exit 1 ) 

dove out è da $1 , 2a è da stdout di dummy , err è da $2 , 3b è da stderr di dummy , e 1 è dal codice di ritorno da dummy .

Si noti che %q nel formato di printf si occupa di quotare, in modo tale che la shell veda gli argomenti (singoli) appropriati quando si tratta di eval . 2a e 3b sono così semplici da essere copiati letteralmente.

Ora per la eval "$({ ... } 2>&1 )"; esterna eval "$({ ... } 2>&1 )"; :

Questo esegue tutto quanto sopra che genera le 2 variabili e l’ exit , lo cattura (quindi il 2>&1 ) e lo analizza nella shell corrente usando eval .

In questo modo vengono impostate le 2 variabili e anche il codice di ritorno.

Q: Usa eval che è male. Quindi è sicuro?

  • Finché printf %q non ha bug, dovrebbe essere sicuro. Ma devi sempre stare molto attento, pensa a ShellShock.

Q: Bugs?

  • Non sono noti bug ovvi, tranne i seguenti:

    • La cattura di grandi quantità di output richiede grande memoria e CPU, dato che tutto va in variabili e deve essere rasterizzato dalla shell. Quindi usalo saggiamente.
    • Come al solito $(echo $'\n\n\n\n') inghiotte tutti i linefeed , non solo l’ultimo. Questo è un requisito POSIX. Se è necessario ottenere il LF illeso, basta aggiungere un carattere finale all’output e rimuoverlo in seguito come nella seguente ricetta (guarda la x finale che consente di leggere un collegamento soft che punta a un file che termina con $'\n' ):

       target="$(readlink -e "$file")x" target="${target%x}" 
    • Le variabili Shell non possono trasportare il byte NUL ( $'\0' ). Semplicemente ignorano se si verificano in stdout o stderr .

  • Il comando dato viene eseguito in una sotto-sotto-shell. Quindi non ha accesso a $PPID , né può alterare le variabili di shell. Puoi catch una funzione di shell, anche builtin, ma quelle non saranno in grado di alterare le variabili di shell (dato che tutto ciò che gira dentro $( .. ) non può farlo). Quindi, se hai bisogno di eseguire una funzione nella shell corrente e catturarla come stderr / stdout, devi farlo nel modo usuale con tempfile s. (Ci sono modi per farlo, che interrompendo la shell normalmente non lascia detriti, ma questo è complesso e merita la sua risposta.)

Q: versione di Bash?

  • Penso che tu abbia bisogno di Bash 4 e superiore (a causa di printf %q )

D: Questo sembra ancora così imbarazzante.

  • Destra. Un’altra risposta qui mostra come può essere fatto in ksh molto più pulito. Comunque non sono abituato a ksh , quindi lo lascio agli altri per creare una ricetta simile facile da riutilizzare per ksh .

D: Perché non usare ksh allora?

  • Perché questa è una soluzione bash

D: Lo script può essere migliorato

  • Ovviamente puoi spremere alcuni byte e creare una soluzione più piccola o più incomprensibile. Provaci 😉

D: C’è un errore di battitura. : catch STDOUT STDERR cmd args.. deve leggere # catch STDOUT STDERR cmd args..

  • In realtà questo è inteso. : presenta in bash -x mentre i commenti vengono silenziosamente ingeriti. Quindi puoi vedere dove si trova il parser se ti capita di avere un refuso nella definizione della funzione. È un vecchio trucco di debug. Ma attenzione, è ansible creare facilmente alcuni effetti collaterali negli argomenti di :

Modifica: aggiunto un altro paio ; per rendere più facile la creazione di un singolo liner fuori da catch() . E ha aggiunto sezione come funziona.

Tecnicamente, le named pipe non sono file temporanei e nessuno qui le menziona. Non memorizzano nulla nel filesystem e puoi eliminarli non appena li colleghi (quindi non li vedrai mai):

 #!/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} 

È ansible avere più processi in background in questo modo e raccogliere in modo asincrono i propri stdout e stderr in un momento opportuno, ecc.

Se hai bisogno di questo solo per un processo, puoi anche usare i numeri fd hardcoded come 3 e 4, invece della {fdout}/{fderr} (che trova un file fd gratuito per te).

Non mi è piaciuta la valutazione, quindi ecco una soluzione che utilizza alcuni trucchi di reindirizzamento per acquisire l’output del programma in una variabile e quindi analizza quella variabile per estrarre i diversi componenti. Il flag -w imposta la dimensione del blocco e influenza l’ordine dei messaggi std-out / err nel formato intermedio. 1 offre una risoluzione potenzialmente elevata a spese di overhead.

 ####### # runs "[email protected]" 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 ) ) } ( ( "[email protected]" | 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 - } 

In breve, credo che la risposta sia “No”. L’acquisizione $( ... ) cattura solo l’output standard della variabile; non c’è un modo per ottenere l’errore standard catturato in una variabile separata. Quindi, quello che hai è tanto pulito quanto diventa.

Che dire … = 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" 

Per il beneficio del lettore ecco una soluzione che utilizza tempfile s.

La domanda non era usare i tempfile . Tuttavia questo potrebbe essere dovuto all’inquinamento indesiderato di /tmp/ con tempfile nel caso in cui la shell muoia. In caso di kill -9 some trap 'rm "$tmpfile1" "$tmpfile2"' 0 non si trap 'rm "$tmpfile1" "$tmpfile2"' 0 .

Se ti trovi in ​​una situazione in cui puoi utilizzare tempfile , ma non vuoi lasciare mai detriti , ecco una ricetta.

Ancora una volta è chiamato catch() (come la mia altra risposta ) e ha la stessa syntax di chiamata:

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

Cosa fa:

  • Crea due file tempfile per stdout e stderr . Tuttavia rimuove quasi immediatamente questi, in modo tale che sono presenti solo per un tempo molto breve.

  • catch_1() cattura lo stdout (FD 1) in una variabile e sposta stderr su stdout , in modo che il prossimo ("left") catch_1 possa prenderlo.

  • L'elaborazione in catch viene eseguita da destra a sinistra, quindi il catch_1 sinistra viene eseguito per ultimo e catch_1 stderr .

Il peggio che può accadere è che alcuni file temporanei appaiono su /tmp/ , ma in questo caso sono sempre vuoti. (Vengono rimossi prima che si riempiano). Di solito questo non dovrebbe essere un problema, dato che in Linux tmpfs supporta approssimativamente 128K file per GB di memoria principale.

  • Il comando dato può accedere e modificare anche tutte le variabili di shell locali. Quindi puoi chiamare una funzione shell che ha effetti collaterali!

  • Questo solo fork due volte per la chiamata tempfile .

bugs:

  • Manca una buona gestione degli errori nel caso in cui tempfile fallisce.

  • Questo fa la solita \n rimozione della shell. Vedi il commento in catch_read() .

  • Non è ansible utilizzare il descrittore di file 66 per inviare dati al proprio comando. Se ti serve, usa un altro descrittore per il reindirizzamento, come 42 (nota che le shell molto vecchie offrono solo FD fino a 9).

  • Questo non può gestire byte NUL ( $'\0' ) in stdout e stderr . (NUL è appena ignorato Per la variante di read tutto ciò che è dietro un NUL viene ignorato).

FYI:

  • Unix ci consente di accedere ai file cancellati, purché tu mantenga qualche riferimento a loro (come un filehandle aperto). In questo modo possiamo aprire e quindi rimuoverli.

Se il comando 1) nessun effetto collaterale stateful e 2) è computazionalmente economico, la soluzione più semplice è semplicemente eseguirla due volte. L’ho usato principalmente per il codice che viene eseguito durante la sequenza di avvio quando non si sa ancora se il disco funzionerà. Nel mio caso si trattava di un piccolissimo some_command quindi non si some_command risultati in termini di prestazioni per il doppio dell’esecuzione e il comando non ha avuto effetti collaterali.

Il vantaggio principale è che questo è pulito e facile da leggere. Le soluzioni qui sono abbastanza intelligenti, ma odio essere quello che deve mantenere uno script contenente le soluzioni più complicate. Consiglierei il semplice approccio run-it-two se il tuo scenario funziona con questo, in quanto è molto più pulito e facile da mantenere.

Esempio:

 output=$(getopt -o '' -l test: -- "[email protected]") errout=$(getopt -o '' -l test: -- "[email protected]" 2>&1 >/dev/null) if [[ -n "$errout" ]]; then echo "Option Error: $errout" fi 

Ancora una volta, questo è solo ok perché doopt non ha effetti collaterali. So che è sicuro per le prestazioni perché il mio codice genitore chiama questo meno di 100 volte durante l’intero programma, e l’utente non noterà mai 100 chiamate getopt contro 200 chiamate getopt.

Ecco una variazione più semplice che non è esattamente ciò che l’OP voleva, ma è diverso da qualsiasi altra opzione. Puoi ottenere quello che vuoi riorganizzando i descrittori di file.

Comando di prova:

 %> cat xx.sh #!/bin/bash echo stdout >&2 echo stderr 

che di per sé fa:

 %> ./xx.sh stdout stderr 

Ora, stampa stdout, acquisisci lo stderr in una variabile e registra lo stdout in un file

 %> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out") stdout %> cat out stdout %> echo $err stderr 

Oppure registra lo stdout e acquisisci lo stderr in una variabile:

 export err=$(./xx.sh 3>&1 1>out 2>&3 ) %> cat out stdout %> echo $err stderr 

Hai un’idea.

Una soluzione alternativa, che è hacky ma forse più intuitiva di alcuni dei suggerimenti in questa pagina, consiste nel taggare i flussi di output, unirli e dividere in seguito in base ai tag. Ad esempio, potremmo etichettare lo stdout con un prefisso “STDOUT”:

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

“ `

Se sai che stdout e / o stderr sono di un formato limitato, puoi creare un tag che non sia in conflitto con il loro contenuto consentito.

ATTENZIONE: NON (ancora?) LAVORO!

Il seguente sembra un ansible vantaggio per farlo funzionare senza creare alcun file temporaneo e anche solo su POSIX sh; tuttavia richiede base64 e, a causa della codifica / decodifica, potrebbe non essere efficiente e utilizzare anche una memoria “più grande”.

  • Anche nel caso semplice, fallirebbe già, quando l’ultima riga di stderr non ha una nuova riga. Questo può essere risolto almeno in alcuni casi sostituendo exe con “{exe; echo> & 2;}”, ovvero aggiungendo una nuova riga.
  • Il problema principale è comunque che tutto sembra filante. Prova a usare un exe come:

    exe () {cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic> & 2}

e vedrete che per esempio parti della linea codificata base64 si trovano nella parte superiore del file, le parti alla fine e il materiale stderr non decodificato nel mezzo.

Bene, anche se l’idea che segue non può essere resa funzionante (cosa che presumo), può servire da anti-esempio per le persone che potrebbero erroneamente credere che potrebbe essere fatto funzionare in questo modo.

Idea (o anti-esempio):

 #!/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 

dà (con la correzione stderr-newline):

 $ ./ggg RAW err1 err2 err3 err4 b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW OUT out1 out2 out3 out4OUT ERR err1 err2 err3 err4ERR 

(Almeno su dash e bash di Debian)