Capture stdout y stderr en diferentes variables

¿Es posible almacenar o capturar stdout y stderr en diferentes variables , sin usar un archivo temporal? Ahora hago esto para obtener stdout in out y stderr en err cuando some_command , pero me gustaría evitar el archivo temporal.

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

Ok, se puso un poco feo, pero aquí hay una solución:

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

donde (echo std; echo err >&2) debe ser reemplazado por el comando real. La salida de stdout se guarda en la matriz $t_std línea por línea omitiendo las nuevas líneas (el -t ) y stderr en $t_err .

Si no te gustan las matrices, puedes hacer

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

que casi imita el comportamiento de var=$(cmd) excepto por el valor de $? que nos lleva a la última modificación:

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

Aquí $? se conserva en $t_ret

Probado en Debian wheezy usando GNU bash , versión 4.2.37 (1) -release (i486-pc-linux-gnu) .

Jonathan tiene la respuesta . Como referencia, este es el truco de ksh93. (requiere una versión no antigua).

 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;} es solo una sustitución de comando que no crea una subcadena. Los comandos se ejecutan en el entorno de shell actual. El espacio al principio es importante ( { es una palabra reservada).

Stderr del grupo de comandos interno se redirige a stdout (para que se aplique a la sustitución interna). A continuación, el stdout de out se asigna a y , y el stderr redirigido se captura por x , sin la pérdida habitual de y a la subshell de sustitución de un comando.

No es posible en otras shells, porque todas las construcciones que capturan resultados requieren poner al productor en una subcaja, que en este caso, incluiría la asignación.

actualización: Ahora también es compatible con mksh.

Este comando establece los valores stdout (stdval) y stderr (errval) en el shell en ejecución actual:

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

siempre que esta función haya sido definida:

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

Cambie el comando exec al comando capturado, ya sea “ls”, “cp”, “df”, etc.


Todo esto se basa en la idea de que podríamos convertir todos los valores capturados en una línea de texto con la ayuda de la función setval, luego setval se usa para capturar cada valor en esta estructura:

 execcommand 2> CaptureErr > CaptureOut 

Convierta cada valor de captura en una llamada setval:

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

Envuelva todo dentro de una llamada de ejecución y repítalo:

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

Obtendrás las llamadas de statement que cada setval crea:

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

Para ejecutar ese código (y obtener los vars establecidos) use eval:

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

y finalmente echo los vars set:

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

También es posible incluir el valor de retorno (salida).
Un ejemplo completo de script bash se ve así:

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

Para resumir todo en beneficio del lector, aquí hay una

Fácil solución de bash reutilizable

Esta versión usa subcapas y se ejecuta sin tempfile . (Para una versión tempfile que se ejecuta sin subcapas, vea mi otra respuesta ).

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

Ejemplo de uso:

 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" 

esto imprime

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

Por lo tanto, se puede usar sin pensarlo más profundamente. Simplemente ponga catch VAR1 VAR2 frente a cualquier command args.. y listo.

Algunos if cmd args..; then if cmd args..; then se convertirá if catch VAR1 VAR2 cmd args..; then if catch VAR1 VAR2 cmd args..; then Realmente nada complejo.

Discusión

P: ¿Cómo funciona?

Simplemente envuelve las ideas de las otras respuestas aquí en una función, de modo que pueda ser reutilizada fácilmente.

catch() básicamente usa eval para establecer las dos variables. Esto es similar a https://stackoverflow.com/a/18086548

Considere una llamada de catch out err dummy 1 2a 3b :

  • vamos a omitir el eval "$({ y el __2="$( por ahora.) Volveré a esto más adelante.

  • __1="$("$("${@:3}")"; } 2>&1; ejecuta dummy 1 2 3 y almacena su stdout en __1 para su uso posterior. Entonces __1 convierte en 2a . También redirige stderr de dummy a stdout , de modo que la captura externa pueda reunir stdout

  • ret=$?; capta el código de salida, que es 1

  • printf '%q=%q\n' "$1" "$__1" >&2; luego saca out=2a a stderr . stderr se usa aquí, ya que el stdout actual ya ha asumido el rol de stderr del comando dummy .

  • exit $ret luego reenvía el código de salida ( 1 ) a la siguiente etapa.

Ahora al exterior __2="$( ... )" :

  • Esto captura el stdout de lo anterior, que es el stderr de la llamada dummy , en la variable __2 . (Podríamos reutilizar __1 aquí, pero utilicé __2 para hacerlo menos confuso). Entonces __2 convierte en 3b

  • ret="$?"; recupera el código de retorno (devuelto) 1 (del dummy ) nuevamente

  • printf '%s=%q\n' "$2" "$__2" >&2; luego envía err=3a a stderr . stderr se usa nuevamente, ya que ya se usó para dar salida a la otra variable 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 catch`.

Tenga en cuenta que, como optimización, podríamos haber escrito esos 2 printf como uno solo como printf '%s=%q\n( exit %q ) “$ __ 2” “$ ret” `también.

Entonces, ¿qué tenemos hasta ahora?

Hemos escrito lo siguiente a stderr:

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

donde out desde $1 , 2a es de stdout de dummy , err es de $2 , 3b es de stderr de dummy , y el 1 es del código de devolución de dummy .

Tenga en cuenta que %q en el formato de printf se encarga de citar, de modo que el shell ve los argumentos adecuados (únicos) cuando se trata de eval . 2a y 3b son tan simples, que se copian literalmente.

Ahora a la eval "$({ ... } 2>&1 )"; externa eval "$({ ... } 2>&1 )"; :

Esto ejecuta todo lo anterior que produce las 2 variables y la exit , lo captura (por lo tanto, el 2>&1 ) y lo analiza en el shell actual usando eval .

De esta forma, las 2 variables se establecen y el código de retorno también.

P: Utiliza eval que es malvado. Entonces, ¿es seguro?

  • Siempre que printf %q no tenga errores, debería ser seguro. Pero siempre debes tener mucho cuidado, solo piensa en ShellShock.

P: ¿Errores?

  • No se conocen errores obvios, excepto los siguientes:

    • La captura de grandes resultados necesita una gran memoria y CPU, ya que todo entra en las variables y necesita ser analizado por el shell. Entonces, úsalo sabiamente
    • Como de costumbre, $(echo $'\n\n\n\n') traga todos los avances de línea , no solo el último. Este es un requisito POSIX. Si necesita obtener las LF ilesas, simplemente agregue algún carácter al final de la salida y elimínelo después como en la siguiente receta (observe la x del final que permite leer un enlace suave que apunta a un archivo que termina en $'\n' )

       target="$(readlink -e "$file")x" target="${target%x}" 
    • Las variables de shell no pueden llevar el byte NUL ( $'\0' ). Simplemente ignoran si ocurren en stdout o stderr .

  • El comando dado se ejecuta en una subcadena secundaria. Por lo tanto, no tiene acceso a $PPID , ni puede alterar las variables de shell. Puede catch una función de shell, incluso builtins, pero esas no podrán alterar las variables de shell (ya que todo lo que se ejecute dentro de $( .. ) no puede hacer esto). Entonces, si necesita ejecutar una función en el shell actual y capturar su stderr / stdout, debe hacer esto de la forma habitual con los tempfile . (Hay formas de hacerlo, tales que la interrupción del caparazón normalmente no deja residuos, pero esto es complejo y merece su propia respuesta).

P: ¿Versión de Bash?

  • Creo que necesitas Bash 4 y superior (debido a printf %q )

P: Esto todavía se ve tan incómodo.

  • Derecha. Otra respuesta aquí muestra cómo se puede hacer en ksh mucho más limpiamente. Sin embargo, no estoy acostumbrado a ksh , por lo que dejo que otros creen una receta similar fácil de reutilizar para ksh .

P: ¿Por qué no usar ksh entonces?

  • Porque esta es una solución bash

P: El script se puede mejorar

  • Por supuesto, puede exprimir algunos bytes y crear una solución más pequeña o más incomprensible. Solo házlo 😉

P: Hay un error tipográfico. : catch STDOUT STDERR cmd args.. leerá # catch STDOUT STDERR cmd args..

  • En realidad esto es intencionado. : aparece en bash -x mientras los comentarios se tragan en silencio. Para que pueda ver dónde está el analizador si tiene un error tipográfico en la definición de la función. Es un viejo truco de depuración. Pero ten cuidado, puedes crear fácilmente efectos secundarios claros dentro de los argumentos de :

Editar: Agregó un par más ; para que sea más fácil crear un único trazador fuera de catch() . Y agregó una sección de cómo funciona.

Técnicamente, las tuberías con nombre no son archivos temporales y nadie aquí las menciona. No almacenan nada en el sistema de archivos y puede eliminarlos tan pronto como los conecte (para que nunca los vea):

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

Puede tener varios procesos en segundo plano de esta manera y recostackr asincrónicamente sus stdouts y stderrs en un momento conveniente, etc.

Si necesita esto solo para un proceso, también puede usar números fd codificados como 3 y 4, en lugar de la {fdout}/{fderr} (que encuentra un fd libre para usted).

No me gustó la evaluación, así que aquí hay una solución que usa algunos trucos de redirección para capturar el resultado del progtwig a una variable y luego analiza esa variable para extraer los diferentes componentes. El distintivo -w establece el tamaño del fragmento e influye en el orden de los mensajes estándar / err en el formato intermedio. 1 da una resolución potencialmente alta a costa de gastos generales.

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

Sucintamente, creo que la respuesta es ‘No’. La captura de $( ... ) solo captura la salida estándar a la variable; no hay una forma de obtener el error estándar capturado en una variable separada. Entonces, lo que tienes es lo mejor posible.

¿Qué pasa con … = 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" 

Para el beneficio del lector, aquí hay una solución que utiliza tempfile .

La pregunta no era usar tempfile . Sin embargo, esto podría deberse a la contaminación no deseada de /tmp/ con tempfile en caso de que el shell muera. En caso de kill -9 una trap 'rm "$tmpfile1" "$tmpfile2"' 0 no se trap 'rm "$tmpfile1" "$tmpfile2"' 0 .

Si se encuentra en una situación en la que puede usar tempfile , pero nunca quiere dejar residuos , aquí hay una receta.

De nuevo, se llama catch() (como mi otra respuesta ) y tiene la misma syntax de llamada:

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

Que hace:

  • Crea dos tempfile s para stdout y stderr . Sin embargo, casi de inmediato los elimina, de modo que solo están disponibles por un tiempo muy corto.

  • catch_1() captura stdout (FD 1) en una variable y mueve stderr a stdout , de modo que la siguiente ("izquierda") catch_1 puede atrapar eso.

  • El procesamiento en la catch se realiza de derecha a izquierda, por lo que el catch_1 izquierdo se ejecuta al final y captura stderr .

Lo peor que puede suceder es que algunos archivos temporales aparecen en /tmp/ , pero siempre están vacíos en ese caso. (Se eliminan antes de que se llenen). Usualmente esto no debería ser un problema, ya que bajo Linux tmpfs soporta aproximadamente 128K archivos por GB de memoria principal.

  • El comando dado también puede acceder y modificar todas las variables de shell locales. ¡Así que puedes llamar a una función de shell que tiene efectos secundarios!

  • Esto solo se tempfile dos veces para la llamada de tempfile .

Loco:

  • Falta un buen manejo de errores en caso de que el tempfile falle.

  • Esto hace la usual \n eliminación del shell. Ver comentario en catch_read() .

  • No puede usar el descriptor de archivo 66 para canalizar datos a su comando. Si lo necesita, use otro descriptor para la redirección, como 42 (tenga en cuenta que los shells muy antiguos solo ofrecen FD hasta 9).

  • Esto no puede manejar los bytes NUL ( $'\0' ) en stdout y stderr . (NUL simplemente se ignora. Para la variante de read , todo lo que está detrás de un NUL se ignora).

FYI:

  • Unix nos permite acceder a los archivos eliminados, siempre y cuando mantenga alguna referencia a ellos (como un identificador de archivo abierto). De esta forma, podemos abrir y luego eliminarlos.

Si el comando 1) no tiene efectos secundarios con estado y 2) es computacionalmente barato, la solución más fácil es simplemente ejecutarlo dos veces. Lo he utilizado principalmente para el código que se ejecuta durante la secuencia de inicio cuando aún no sabe si el disco va a funcionar. En mi caso, era un pequeño some_command por lo que no se some_command rendimiento dos veces y el comando no tuvo efectos secundarios.

El principal beneficio es que es limpio y fácil de leer. Las soluciones aquí son bastante ingeniosas, pero odiaría ser el que tiene que mantener un script que contenga las soluciones más complicadas. Yo recomendaría el enfoque simple de ejecutar dos veces si su escenario funciona con eso, ya que es mucho más limpio y fácil de mantener.

Ejemplo:

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

De nuevo, esto solo está bien, porque getopt no tiene efectos secundarios. Sé que es seguro para el rendimiento porque mi código padre lo llama menos de 100 veces durante todo el progtwig, y ​​el usuario nunca notará 100 llamadas getopt frente a 200 llamadas getopt.

Aquí hay una variación más simple que no es exactamente lo que el OP quería, pero no se parece a ninguna de las otras opciones. Puedes obtener lo que quieras reorganizando los descriptores de archivos.

Comando de prueba:

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

que en sí mismo hace:

 %> ./xx.sh stdout stderr 

Ahora, imprima stdout, capture stderr en una variable y log stdout en un archivo

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

O log stdout y capture stderr a una variable:

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

Entiendes la idea.

Una solución alternativa, que es chistosa pero quizás más intuitiva que algunas de las sugerencias en esta página, es etiquetar las secuencias de salida, fusionarlas y dividirlas después según las tags. Por ejemplo, podríamos etiquetar stdout con un prefijo “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") 

“ `

Si sabe que stdout y / o stderr tienen una forma restringida, puede crear una etiqueta que no entre en conflicto con su contenido permitido.

ADVERTENCIA: NO (¿todavía?) TRABAJANDO!

Lo siguiente parece una posible ventaja para que funcione sin crear ningún archivo temporal y solo en POSIX sh; sin embargo, requiere base64 y debido a la encoding / deencoding puede que no sea tan eficiente y use también memoria “más grande”.

  • Incluso en el caso simple, ya no funcionaría, cuando la última línea de stderr no tiene nueva línea. Esto se puede solucionar al menos en algunos casos con la sustitución de exe por “{exe; echo> & 2;}”, es decir, agregar una nueva línea.
  • El principal problema es, sin embargo, que todo parece atrevido. Intenta usar un exe como:

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

y verá que, por ejemplo, las partes de la línea codificada en base64 están en la parte superior del archivo, las partes al final y las cosas de stderr no decodificadas en el medio.

Bueno, incluso si la siguiente idea no se puede hacer funcionar (lo que supongo), puede servir como un anti-ejemplo para las personas que pueden creer falsamente que podría funcionar así.

Idea (o anti-ejemplo):

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

da (con la corrección stderr-newline):

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

(Al menos en el tablero y el bash de Debian)