Cómo almacenar el error estándar en una variable

182

Digamos que tengo un script como el siguiente:

useless.sh

echo "This Is Error" 1>&2
echo "This Is Output" 

Y tengo otro script de shell:

alsoUseless.sh

./useless.sh | sed 's/Output/Useless/'

Quiero capturar "This Is Error", o cualquier otro stderr de useless.sh, en una variable. Llamémoslo ERROR.

Tenga en cuenta que estoy usando stdout para algo. Quiero seguir usando stdout, por lo que redirigir stderr a stdout no es útil, en este caso.

Entonces, básicamente, quiero hacer

./useless.sh 2> $ERROR | ...

pero eso obviamente no funciona.

También sé que podría hacer

./useless.sh 2> /tmp/Error
ERROR=`cat /tmp/Error`

Pero eso es feo e innecesario.

Desafortunadamente, si no aparecen respuestas aquí, eso es lo que voy a tener que hacer.

Espero que haya otra forma.

Alguien tiene alguna idea mejor?

psycotica0
fuente
44
¿Para qué exactamente quieres usar stdout? ¿Simplemente quieres verlo en la consola? ¿O estás capturando / redirigiendo su salida? Si es solo a la consola, redirige stdout a consola y stderr a stdout para capturarlo:ERROR=$(./useless.sh | sed 's/Output/Useless/' 2>&1 1>/dev/ttyX)
Tim Kersten

Respuestas:

91

Sería mejor capturar el archivo de error así:

ERROR=$(</tmp/Error)

El shell reconoce esto y no tiene que ejecutarse ' cat' para obtener los datos.

La pregunta más grande es difícil. No creo que haya una manera fácil de hacerlo. Tendría que construir toda la canalización en el subconjunto, eventualmente enviando su salida estándar final a un archivo, para que pueda redirigir los errores a la salida estándar.

ERROR=$( { ./useless.sh | sed s/Output/Useless/ > outfile; } 2>&1 )

Tenga en cuenta que el punto y coma es necesario (en conchas clásicas, Bourne, Korn, seguro; probablemente también en Bash). El ' {}' realiza la redirección de E / S sobre los comandos adjuntos. Tal como está escrito, también capturaría errores sed.

ADVERTENCIA: Código no probado formalmente - uso bajo su propio riesgo.

Jonathan Leffler
fuente
1
Tenía la esperanza de que hubiera algún truco realmente loco que no conocía, pero parece que es así. Gracias.
psycotica0
9
Si no necesita la salida estándar, puede redirigirla en /dev/nulllugar de outfile(si es como yo, encontró esta pregunta a través de Google y no tiene los mismos requisitos que el OP)
Mark Eirich
2
Para obtener una respuesta sin archivos temporales, consulte aquí .
Tom Hale
1
Aquí hay una manera de hacerlo sin redirigirlo a archivos; juega con el intercambio stdouty stderradelante y atrás. Pero cuidado , como se dice aquí : en bash, sería mejor no asumir que el descriptor de archivo 3 no se
usa
69

alsoUseless.sh

Esto le permitirá canalizar la salida de su useless.shscript a través de un comando como sedy guardarlo stderren una variable llamada error. El resultado de la tubería se envía stdoutpara su visualización o para ser canalizado a otro comando.

Configura un par de descriptores de archivo adicionales para administrar las redirecciones necesarias para hacer esto.

#!/bin/bash

exec 3>&1 4>&2 #set up extra file descriptors

error=$( { ./useless.sh | sed 's/Output/Useless/' 2>&4 1>&3; } 2>&1 )

echo "The message is \"${error}.\""

exec 3>&- 4>&- # release the extra file descriptors
Pausado hasta nuevo aviso.
fuente
44
Es una buena técnica usar 'exec' para establecer y cerrar descriptores de archivo. El cierre no es realmente necesario si el script sale inmediatamente después.
Jonathan Leffler
3
¿Cómo capturaría ambas stderry stdouten variables?
Gingi
Excelente. Esto me ayuda a implementar una dry_runfunción que puede elegir de manera confiable entre hacer eco de sus argumentos y ejecutarlos, independientemente de si el comando que se ejecuta en seco se canaliza a otro archivo.
Mihai Danila
1
@ t00bs: readno acepta entradas de una tubería. Puede usar otras técnicas para lograr lo que está tratando de demostrar.
Pausado hasta nuevo aviso.
2
Podría ser más simple, con: error = $ (./useless.sh | sed 's / Output / Useless /' 2> & 1 1> & 3)
Jocelyn
64

Stderr redirigido a stdout, stdout a / dev / null, y luego use los backticks o $()para capturar el stderr redirigido:

ERROR=$(./useless.sh 2>&1 >/dev/null)
Chas Owens
fuente
8
Esta es la razón por la que incluí la tubería en mi ejemplo. Todavía quiero la salida estándar, y quiero que haga otras cosas, vaya a otros lugares.
psycotica0
Para los comandos que envían resultados solo a stderr, la forma simple de capturarlos es, por ejemploPY_VERSION="$(python --version 2>&1)"
John Mark
9

Hay muchos duplicados para esta pregunta, muchos de los cuales tienen un escenario de uso ligeramente más simple en el que no desea capturar stderr y stdout y el código de salida, todo al mismo tiempo.

if result=$(useless.sh 2>&1); then
    stdout=$result
else
    rc=$?
    stderr=$result
fi

funciona para el escenario común donde se espera una salida adecuada en caso de éxito o un mensaje de diagnóstico en stderr en caso de falla.

Tenga en cuenta que las declaraciones de control del shell ya se examinan $?bajo el capó; así que todo lo que parece

cmd
if [ $? -eq 0 ], then ...

es solo una forma torpe y unidiomática de decir

if cmd; then ...
tripleee
fuente
Esto funcionó para mí: my_service_status = $ (service my_service status 2> & 1) ¡Gracias!
JRichardsz
6
# command receives its input from stdin.
# command sends its output to stdout.
exec 3>&1
stderr="$(command </dev/stdin 2>&1 1>&3)"
exitcode="${?}"
echo "STDERR: $stderr"
exit ${exitcode}
human9
fuente
1
commandes una mala elección aquí, ya que en realidad hay un nombre incorporado con ese nombre. Podría hacerlo yourCommando algo así, para ser más explícito.
Charles Duffy
4

Para beneficio del lector, esta receta aquí

  • se puede volver a usar como línea para atrapar stderr en una variable
  • todavía da acceso al código de retorno del comando
  • Sacrifica un descriptor de archivo temporal 3 (que usted puede cambiar, por supuesto)
  • Y no expone estos descriptores de archivos temporales al comando interno

Si desea captura stderrde algunos commanden varque puede hacer

{ var="$( { command; } 2>&1 1>&3 3>&- )"; } 3>&1;

Después lo tienes todo:

echo "command gives $? and stderr '$var'";

Si commandes simple (no algo así a | b), puede dejar el interior {}alejado:

{ var="$(command 2>&1 1>&3 3>&-)"; } 3>&1;

Envuelto en una función fácil de reutilizar bash(probablemente necesita la versión 3 y superior para local -n):

: catch-stderr var cmd [args..]
catch-stderr() { local -n v="$1"; shift && { v="$("$@" 2>&1 1>&3 3>&-)"; } 3>&1; }

Explicado:

  • local -n alias "$ 1" (que es la variable para catch-stderr )
  • 3>&1 usa el descriptor de archivo 3 para guardar sus puntos estándar
  • { command; } (o "$ @") luego ejecuta el comando dentro de la captura de salida $(..)
  • Tenga en cuenta que el orden exacto es importante aquí (hacerlo de forma incorrecta baraja los descriptores de archivo incorrectamente):
    • 2>&1redirige stderra la captura de salida$(..)
    • 1>&3redirige stdoutlejos de la captura de salida de $(..)nuevo al "exterior" stdoutque se guardó en el descriptor de archivo 3. Tenga en cuenta questderr todavía se refiere a donde FD 1 señaló antes: a la captura de salida$(..)
    • 3>&-luego cierra el descriptor de archivo 3 ya que ya no es necesario, de modo que commandno aparece repentinamente un descriptor de archivo abierto desconocido. Tenga en cuenta que la carcasa externa todavía tiene FD 3 abierto, perocommand no lo verá.
    • Esto último es importante, porque algunos programas como lvmquejarse de descriptores de archivos inesperados. Y se lvmqueja stderr, ¡justo lo que vamos a capturar!

Puede capturar cualquier otro descriptor de archivo con esta receta, si se adapta en consecuencia. Excepto el descriptor de archivo 1, por supuesto (aquí la lógica de redireccionamiento sería incorrecta, pero para el descriptor de archivo 1 puede usarvar=$(command) como de costumbre).

Tenga en cuenta que esto sacrifica el descriptor de archivo 3. Si necesita ese descriptor de archivo, no dude en cambiar el número. Pero tenga en cuenta que algunos proyectiles (de la década de 1980) podrían entenderse 99>&1como un argumento 9seguido de 9>&1(esto no es un problema para bash).

También tenga en cuenta que no es particularmente fácil hacer que este FD 3 sea configurable a través de una variable. Esto hace que las cosas sean muy ilegibles:

: catch-var-from-fd-by-fd variable fd-to-catch fd-to-sacrifice command [args..]
catch-var-from-fd-by-fd()
{
local -n v="$1";
local fd1="$2" fd2="$3";
shift 3 || return;

eval exec "$fd2>&1";
v="$(eval '"$@"' "$fd1>&1" "1>&$fd2" "$fd2>&-")";
eval exec "$fd2>&-";
}

Nota de seguridad: los primeros 3 argumentos catch-var-from-fd-by-fdno deben tomarse de un tercero. Dales siempre explícitamente de forma "estática".

Entonces no-no-no catch-var-from-fd-by-fd $var $fda $fdb $command , , ¡nunca hagas esto!

Si pasa un nombre de variable variable, al menos hágalo de la siguiente manera: local -n var="$var"; catch-var-from-fd-by-fd var 3 5 $command

Esto aún no lo protegerá contra cada exploit, pero al menos ayuda a detectar y evitar errores comunes de secuencias de comandos.

Notas:

  • catch-var-from-fd-by-fd var 2 3 cmd.. es lo mismo que catch-stderr var cmd..
  • shift || returnes solo una forma de evitar errores feos en caso de que olvide dar el número correcto de argumentos. Quizás terminar el shell sería otra forma (pero esto dificulta la prueba desde la línea de comandos).
  • La rutina fue escrita de tal manera que es más fácil de entender. Uno puede reescribir la función de manera que no la necesite exec, pero luego se vuelve realmente fea.
  • Esta rutina puede ser reescrita para no bashtan bien que no hay necesidad de hacerlo local -n. Sin embargo, entonces no puedes usar variables locales y ¡se vuelve extremadamente feo!
  • También tenga en cuenta que los evals se utilizan de manera segura. Por evallo general, se considera peligroso. Sin embargo, en este caso no es más malvado que usar "$@"(para ejecutar comandos arbitrarios). Sin embargo, asegúrese de utilizar la cita exacta y correcta como se muestra aquí (de lo contrario, se vuelve muy muy peligroso ).
Tino
fuente
3

Así es como lo hice:

#
# $1 - name of the (global) variable where the contents of stderr will be stored
# $2 - command to be executed
#
captureStderr()
{
    local tmpFile=$(mktemp)

    $2 2> $tmpFile

    eval "$1=$(< $tmpFile)"

    rm $tmpFile
}

Ejemplo de uso:

captureStderr err "./useless.sh"

echo -$err-

Se hace uso de un archivo temporal. Pero al menos las cosas feas están envueltas en una función.

tfga
fuente
@ShadowWizard Poca duda de mi parte. En francés, el colon suele ir precedido de un espacio. Por error, aplico esta misma regla con respuestas en inglés . Después de verificar esto , sé que no volveré a cometer este error.
Stephan
@Stephan aplausos, esto también se ha discutido aquí . :)
Shadow Wizard es Ear For You
1
Hay formas más seguras de hacerlo que usar eval. Por ejemplo, printf -v "$1" '%s' "$(<tmpFile)"no corre el riesgo de ejecutar código arbitrario si su TMPDIRvariable se ha establecido en un valor malicioso (o si el nombre de la variable de destino contiene dicho valor).
Charles Duffy
1
Del mismo modo, rm -- "$tmpFile"es más robusto que rm $tmpFile.
Charles Duffy
2

Este es un problema interesante para el que esperaba que hubiera una solución elegante. Lamentablemente, termino con una solución similar al Sr. Leffler, pero agregaré que puede llamar inútil desde dentro de una función Bash para mejorar la legibilidad:

#! / bin / bash

función inútil {
    /tmp/useless.sh | sed 's / Salida / Inútil /'
}

ERROR = $ (inútil)
echo $ ERROR

Todo otro tipo de redirección de salida debe estar respaldado por un archivo temporal.


fuente
2

POSIX

STDERR se puede capturar con algo de magia de redireccionamiento:

$ { error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&3 ; } 2>&1); } 3>&1
lrwxrwxrwx 1 rZZt rZZt 7 Aug 22 15:44 /bin -> usr/bin/

$ echo $error
ls: cannot access '/XXXX': No such file or directory

Tenga en cuenta que la canalización de STDOUT del comando (aquí ls) se realiza dentro del más interno { }. Si está ejecutando un comando simple (p. Ej., No una tubería), puede eliminar estas llaves internas.

No puede canalizar fuera del comando, ya que la tubería crea una subshell en bashy zsh, y la asignación a la variable en la subshell no estaría disponible para el shell actual.

intento

En bash, sería mejor no asumir que el descriptor de archivo 3 no se utiliza:

{ error=$( { { ls -ld /XXXX /bin | tr o Z ; } 1>&$tmp ; } 2>&1); } {tmp}>&1; 
exec {tmp}>&-  # With this syntax the FD stays open

Tenga en cuenta que esto no funciona zsh.


Gracias a esta respuesta por la idea general.

Tom Hale
fuente
¿Puedes explicar esta línea con detalles? no entendió 1> & $ tmp; {error = $ ({{ls -ld / XXXX / bin | tr o Z;} 1> & $ tmp;} 2> & 1); } {tmp}> & 1;
Thiago Conrado
1

Esta publicación me ayudó a encontrar una solución similar para mis propios fines:

MESSAGE=`{ echo $ERROR_MESSAGE | format_logs.py --level=ERROR; } 2>&1`

Entonces, siempre que nuestro MENSAJE no sea una cadena vacía, la pasamos a otras cosas. Esto nos permitirá saber si nuestro format_logs.py falló con algún tipo de excepción de Python.

palmbardier
fuente
1

Capturar e imprimir stderr

ERROR=$( ./useless.sh 3>&1 1>&2 2>&3 | tee /dev/fd/2 )

Descompostura

Puede usarlo $()para capturar stdout, pero desea capturar stderr en su lugar. Entonces intercambias stdout y stderr. Usar fd 3 como almacenamiento temporal en el algoritmo de intercambio estándar.

Si desea capturar e imprimir, use teepara hacer un duplicado. En este caso, la salida de teese capturará en $()lugar de ir a la consola, pero stderr (of tee) seguirá yendo a la consola, por lo que la usamos como la segunda salida a teetravés del archivo especial, /dev/fd/2ya que teeespera una ruta de archivo en lugar de un fd número.

NOTA: Esa es una gran cantidad de redireccionamientos en una sola línea y el orden es importante. $()está agarrando el stdout de teeal final de la tubería y la tubería en sí enruta stdout de ./useless.shal stdin de teeDESPUÉS de que intercambiamos stdin y stdout por ./useless.sh.

Usando stdout de ./useless.sh

El OP dijo que todavía quería usar (no solo imprimir) stdout, como ./useless.sh | sed 's/Output/Useless/'.

No hay problema, solo hágalo ANTES de intercambiar stdout y stderr. Recomiendo moverlo a una función o archivo (también-useless.sh) y llamarlo en lugar de ./useless.sh en la línea de arriba.

Sin embargo, si desea CAPTURAR stdout Y stderr, creo que debe recurrir a archivos temporales porque $() solo lo hará de uno en uno y creará una subshell desde la que no puede devolver variables.

SensorSmith
fuente
1

Iterando un poco la respuesta de Tom Hale, descubrí que es posible envolver el yoga de redirección en una función para una reutilización más fácil. Por ejemplo:

#!/bin/sh

capture () {
    { captured=$( { { "$@" ; } 1>&3 ; } 2>&1); } 3>&1
}

# Example usage; capturing dialog's output without resorting to temp files
# was what motivated me to search for this particular SO question
capture dialog --menu "Pick one!" 0 0 0 \
        "FOO" "Foo" \
        "BAR" "Bar" \
        "BAZ" "Baz"
choice=$captured

clear; echo $choice

Es casi seguro que sea posible simplificar esto aún más. No lo he probado especialmente, pero parece funcionar tanto con bash como con ksh.

AmarilloApple
fuente
0

Si desea omitir el uso de un archivo temporal, puede usar la sustitución del proceso. Todavía no lo he conseguido para trabajar. Este fue mi primer intento:

$ .useless.sh 2> >( ERROR=$(<) )
-bash: command substitution: line 42: syntax error near unexpected token `)'
-bash: command substitution: line 42: `<)'

Entonces intenté

$ ./useless.sh 2> >( ERROR=$( cat <() )  )
This Is Output
$ echo $ERROR   # $ERROR is empty

sin embargo

$ ./useless.sh 2> >( cat <() > asdf.txt )
This Is Output
$ cat asdf.txt
This Is Error

Entonces, la sustitución del proceso generalmente está haciendo lo correcto ... desafortunadamente, cada vez que envuelvo STDIN dentro >( )con algo $()en un intento de capturar eso en una variable, pierdo el contenido de $(). Creo que esto se debe a que $()inicia un subproceso que ya no tiene acceso al descriptor de archivo en / dev / fd, propiedad del proceso padre.

La sustitución de procesos me ha comprado la capacidad de trabajar con un flujo de datos que ya no está en STDERR, desafortunadamente no parece que pueda manipularlo de la manera que quiero.

Barton Chittenden
fuente
1
Si lo hiciera ./useless.sh 2> >( ERROR=$( cat <() ); echo "$ERROR" ), vería la salida de ERROR. El problema es que la sustitución del proceso se ejecuta en un shell secundario, por lo que el valor establecido en el shell secundario no afecta al shell primario.
Jonathan Leffler
0
$ b=$( ( a=$( (echo stdout;echo stderr >&2) ) ) 2>&1 )
$ echo "a=>$a b=>$b"
a=>stdout b=>stderr
Mario Wolff
fuente
3
Parece una buena idea, pero en Mac OSX 10.8.5, imprimea=> b=>stderr
Heath Borders
3
Estoy de acuerdo con @HeathBorders; Esto no produce la salida mostrada. El problema aquí es que ase evalúa y se asigna en un sub-shell, y la asignación en el sub-shell no afecta al shell principal. (Probado en Ubuntu 14.04 LTS y Mac OS X 10.10.1.)
Jonathan Leffler
Lo mismo en Windows GitBash. Entonces, no funciona. ( GNU bash, version 4.4.12(1)-release (x86_64-pc-msys))
Kirby
Tampoco funciona SLE 11.4y produce el efecto descrito por @JonathanLeffler
smarber
Si bien este código puede responder a la pregunta, proporcionar un contexto adicional con respecto a por qué y / o cómo este código responde a la pregunta mejora su valor a largo plazo.
β.εηοιτ.βε
0

En zsh:

{ . ./useless.sh > /dev/tty } 2>&1 | read ERROR
$ echo $ERROR
( your message )
Ray Andrews
fuente
0

Para errores de prueba de sus comandos:

execute [INVOKING-FUNCTION] [COMMAND]

execute () {
    function="${1}"
    command="${2}"
    error=$(eval "${command}" 2>&1 >"/dev/null")

    if [ ${?} -ne 0 ]; then
        echo "${function}: ${error}"
        exit 1
    fi
}

Inspirado en la fabricación ajustada:

Alberto Salvia Novella
fuente
La solución idiomática es eliminar la asignación dentro del if. Déjame publicar una solución por separado.
tripleee
0

Una solución simple

{ ERROR=$(./useless.sh 2>&1 1>&$out); } {out}>&1
echo "-"
echo $ERROR

Producirá:

This Is Output
-
This Is Error
K - La toxicidad en SO está creciendo.
fuente
0

Mejorando la respuesta de YellowApple :

Esta es una función Bash para capturar stderr en cualquier variable

stderr_capture_example.sh:

#!/usr/bin/env bash

# Capture stderr from a command to a variable while maintaining stdout
# @Args:
# $1: The variable name to store the stderr output
# $2: Vararg command and arguments
# @Return:
# The Command's Returnn-Code or 2 if missing arguments
function capture_stderr {
  [ $# -lt 2 ] && return 2
  local stderr="$1"
  shift
  {
    printf -v "$stderr" '%s' "$({ "$@" 1>&3; } 2>&1)"
  } 3>&1
}

# Testing with a call to erroring ls
LANG=C capture_stderr my_stderr ls "$0" ''

printf '\nmy_stderr contains:\n%s' "$my_stderr"

Pruebas:

bash stderr_capture_example.sh

Salida:

 stderr_capture_example.sh

my_stderr contains:
ls: cannot access '': No such file or directory

Esta función se puede utilizar para capturar la elección devuelta de un dialogcomando.

Léa Gris
fuente