bash - examples - Capture stdout y stderr en diferentes variables
awk unix (13)
¿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
¿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"
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.
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 declaración 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|"
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 -p x y # Show the values
produce
x=stderr
y=stdout
La sintaxis ${ 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.
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 programa 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 -
}
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) .
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 share ) y tiene la misma sintaxis 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 parastdout
ystderr
. Sin embargo, casi de inmediato los elimina, de modo que solo están disponibles por un tiempo muy corto.catch_1()
capturastdout
(FD 1) en una variable y muevestderr
astdout
, 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 elcatch_1
izquierdo se ejecuta al final y capturastderr
.
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 detempfile
.
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 encatch_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, como42
(tenga en cuenta que los shells muy antiguos solo ofrecen FD hasta 9).Esto no puede manejar los bytes NUL (
$''/0''
) enstdout
ystderr
. (NUL simplemente se ignora. Para la variante deread
, 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.
Para sum todo up 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 share
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;
ejecutadummy 1 2 3
y almacena sustdout
en__1
para su uso posterior. Entonces__1
convierte en2a
. También redirigestderr
dedummy
astdout
, de modo que la captura externa pueda reunirstdout
ret=$?;
capta el código de salida, que es1
printf ''%q=%q/n'' "$1" "$__1" >&2;
luego sacaout=2a
astderr
.stderr
se usa aquí, ya que elstdout
actual ya ha asumido el rol destderr
del comandodummy
.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 elstderr
de la llamadadummy
, en la variable__2
. (Podríamos reutilizar__1
aquí, pero utilicé__2
para hacerlo menos confuso). Entonces__2
convierte en3b
ret="$?";
recupera el código de retorno (devuelto)1
(deldummy
) nuevamenteprintf ''%s=%q/n'' "$2" "$__2" >&2;
luego envíaerr=3a
astderr
.stderr
se usa nuevamente, ya que ya se usó para dar salida a la otra variableout=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 lax
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 enstdout
ostderr
.
El comando dado se ejecuta en una subcadena secundaria. Por lo tanto, no tiene acceso a
$PPID
, ni puede alterar las variables de shell. Puedecatch
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 lostempfile
. (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 aksh
, por lo que dejo que otros creen una receta similar fácil de reutilizar paraksh
.
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 enbash -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.
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 programa, y el usuario nunca notará 100 llamadas getopt frente a 200 llamadas getopt.
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.
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}<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
Puede tener varios procesos en segundo plano de esta manera y recopilar 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).
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 etiquetas. 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 codificación / decodificación 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)