Captura de códigos de error en una tubería de shell

97

Actualmente tengo un script que hace algo como

./a | ./b | ./c

Quiero modificarlo para que si alguno de a, bo c sale con un código de error, imprimo un mensaje de error y me detengo en lugar de enviar una salida incorrecta.

¿Cuál sería la forma más sencilla / limpia de hacerlo?

hugomg
fuente
7
Realmente tiene que haber algo como &&|lo que significaría "solo continuar con la tubería si el comando anterior fue exitoso". Supongo que también podría haberlo hecho, |||lo que significaría "continuar la tubería si el comando anterior falló" (y posiblemente enviar el mensaje de error como Bash 4 |&).
Pausado hasta nuevo aviso.
6
@DennisWilliamson, no se puede "detener la tubería", porque a, b, ccomandos no se ejecutan secuencialmente pero en paralelo. En otras palabras, los flujos de datos de forma secuencial a partir ade c, pero el real a, by clas órdenes de marcha (más o menos) al mismo tiempo.
Giacomo

Respuestas:

20

Si realmente no desea que el segundo comando continúe hasta que se sepa que el primero es exitoso, entonces probablemente necesite usar archivos temporales. La versión simple de eso es:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

La redirección '1> & 2' también se puede abreviar '> & 2'; sin embargo, una versión anterior del shell MKS manejó mal la redirección de errores sin el '1' anterior, por lo que he usado esa notación inequívoca para la confiabilidad durante años.

Esto filtra archivos si interrumpe algo. Usos de programación de shell a prueba de bombas (más o menos):

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

La primera línea de trampa dice 'ejecutar los comandos' rm -f $tmp.[12]; exit 1'cuando ocurre alguna de las señales 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE o 15 SIGTERM, o 0 (cuando el shell sale por cualquier motivo). Si está escribiendo un script de shell, la trampa final solo necesita eliminar la trampa en 0, que es la trampa de salida de shell (puede dejar las otras señales en su lugar ya que el proceso está a punto de terminar de todos modos).

En la canalización original, es factible que 'c' lea datos de 'b' antes de que 'a' haya terminado; esto suele ser deseable (por ejemplo, da trabajo a varios núcleos). Si 'b' es una fase de 'clasificación', entonces esto no se aplicará; 'b' tiene que ver todas sus entradas antes de poder generar cualquiera de sus salidas.

Si desea detectar qué comando (s) fallan, puede usar:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Esto es simple y simétrico: es trivial extenderlo a una tubería de 4 o N partes.

La simple experimentación con 'set -e' no ayudó.

Jonathan Leffler
fuente
1
Recomendaría usar mktempo tempfile.
Pausado hasta nuevo aviso.
@Dennis: sí, supongo que debería acostumbrarme a comandos como mktemp o tmpfile; no existían a nivel de caparazón cuando lo aprendí, oh, hace tantos años. Hagamos una comprobación rápida. Encuentro mktemp en MacOS X; Tengo mktemp en Solaris pero solo porque instalé herramientas GNU; parece que mktemp está presente en HP-UX antiguo. No estoy seguro de si existe una invocación común de mktemp que funcione en todas las plataformas. POSIX no estandariza ni mktemp ni tmpfile. No encontré tmpfile en las plataformas a las que tengo acceso. En consecuencia, no podré usar los comandos en scripts de shell portátiles.
Jonathan Leffler
1
Al usarlo, traptenga en cuenta que el usuario siempre puede enviar SIGKILLa un proceso para finalizarlo inmediatamente, en cuyo caso su trampa no surtirá efecto. Lo mismo ocurre cuando su sistema experimenta un corte de energía. Al crear archivos temporales, asegúrese de usarlo mktempporque eso colocará sus archivos en algún lugar, donde se limpiarán después de reiniciar (generalmente en /tmp).
Josch
155

En bash puedes usar set -ey set -o pipefailal principio de tu archivo. Un comando posterior ./a | ./b | ./cfallará cuando falla cualquiera de los tres scripts. El código de retorno será el código de retorno del primer script fallido.

Tenga en cuenta que pipefailno está disponible en sh estándar .

Michel Samia
fuente
No sabía nada de pipefail, realmente útil.
Phil Jackson
Está pensado para ponerlo en un script, no en el shell interactivo. Su comportamiento se puede simplificar a set -e; falso; también sale del proceso de shell, que es el comportamiento esperado;)
Michel Samia
11
Nota: esto seguirá ejecutando los tres scripts y no detendrá la tubería en el primer error.
hyde
1
@ n2liquid-GuilhermeVieira Por cierto, por "variaciones diferentes", quise decir específicamente, eliminar una o ambas set(para 4 versiones diferentes en total), y ver cómo eso afecta la salida de la última echo.
Hyde
1
@josch Encontré esta página cuando buscaba cómo hacer esto en bash de google, y sin embargo, "Esto es exactamente lo que estoy buscando". Sospecho que muchas personas que votan a favor de las respuestas pasan por un proceso de pensamiento similar y no verifican las etiquetas y las definiciones de las etiquetas.
Troy Daniels
43

También puede verificar la ${PIPESTATUS[]}matriz después de la ejecución completa, por ejemplo, si ejecuta:

./a | ./b | ./c

Luego ${PIPESTATUS}habrá una matriz de códigos de error de cada comando en la tubería, por lo que si el comando del medio falla, echo ${PIPESTATUS[@]}contendrá algo como:

0 1 0

y algo como esto se ejecuta después del comando:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

le permitirá comprobar que todos los comandos de la tubería se ejecutaron correctamente.

Imron
fuente
11
Esto es bashish --- es una extensión de bash y no es parte del estándar Posix, por lo que otros shells como dash y ash no lo admitirán. Esto significa que puede tener problemas si intenta usarlo en scripts que se inician #!/bin/sh, porque si shno es bash, no funcionará. (Se puede arreglar fácilmente recordando usar #!/bin/bashen su lugar.)
David dado el
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- devuelve 0 si $PIPESTATUS[@]solo contiene 0 y espacios (si todos los comandos en la tubería tienen éxito).
MattBianco
@MattBianco Esta es la mejor solución para mí. También funciona con &&, por ejemplo, command1 && command2 | command3si alguno de ellos falla, su solución devuelve un valor distinto de cero.
DavidC
8

Desafortunadamente, la respuesta de Johnathan requiere archivos temporales y las respuestas de Michel e Imron requieren bash (aunque esta pregunta está etiquetada como shell). Como ya han señalado otros, no es posible abortar la tubería antes de que se inicien procesos posteriores. Todos los procesos se inician a la vez y, por lo tanto, se ejecutarán antes de que se pueda comunicar cualquier error. Pero el título de la pregunta también preguntaba sobre códigos de error. Estos se pueden recuperar e investigar después de que la tubería terminó para determinar si alguno de los procesos involucrados falló.

Aquí hay una solución que detecta todos los errores en la tubería y no solo los errores del último componente. Así que esto es como el error de tubería de bash, solo que más poderoso en el sentido de que puede recuperar todos los códigos de error.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Para detectar si algo falló, se echoimprime un comando en el error estándar en caso de que falle algún comando. Luego, la salida de error estándar combinada se guarda $resy se investiga más tarde. Esta es también la razón por la que el error estándar de todos los procesos se redirige a la salida estándar. También puede enviar esa salida /dev/nullo dejarla como otro indicador más de que algo salió mal. Puede reemplazar la última redirección a /dev/nullcon un archivo si desea almacenar la salida del último comando en cualquier lugar.

Para jugar más con esta construcción y convencerte de que realmente hace lo que debería, reemplacé ./a, ./by ./cpor subcapas que se ejecutan echo, caty exit. Puede usar esto para verificar que esta construcción realmente reenvía toda la salida de un proceso a otro y que los códigos de error se registran correctamente.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
Josch
fuente