Verificar el estado de salida de Bash de varios comandos de manera eficiente

Resuelto Baldrick asked hace 13 años • 16 respuestas

¿Existe algo similar a pipefail para múltiples comandos, como una declaración 'try' pero dentro de bash? Me gustaría hacer algo como esto:

echo "trying stuff"
try {
    command1
    command2
    command3
}

Y en cualquier momento, si algún comando falla, abandone y repita el error de ese comando. No quiero tener que hacer algo como:

command1
if [ $? -ne 0 ]; then
    echo "command1 borked it"
fi

command2
if [ $? -ne 0 ]; then
    echo "command2 borked it"
fi

Y así sucesivamente... o algo como:

pipefail -o
command1 "arg1" "arg2" | command2 "arg1" "arg2" | command3

Porque creo que los argumentos de cada comando (corríjanme si me equivoco) interferirán entre sí. Estos dos métodos me parecen terriblemente prolijos y desagradables, así que estoy aquí pidiendo un método más eficiente.

Baldrick avatar Mar 04 '11 22:03 Baldrick
Aceptado

Puede escribir una función que inicie y pruebe el comando por usted. Supongamos command1que y command2son variables de entorno que se han configurado para un comando.

function mytest {
    "$@"
    local status=$?
    if (( status != 0 )); then
        echo "error with $1" >&2
    fi
    return $status
}

mytest "$command1"
mytest "$command2"
krtek avatar Mar 04 '2011 15:03 krtek

¿Qué quieres decir con "abandonar y repetir el error"? Si quiere decir que desea que el script finalice tan pronto como falle cualquier comando, simplemente haga

set -e    # DON'T do this.  See commentary below.

al comienzo del script (pero tenga en cuenta la advertencia a continuación). No se moleste en repetir el mensaje de error: deje que el comando fallido se encargue de eso. En otras palabras, si lo haces:

#!/bin/sh

set -e    # Use caution.  eg, don't do this
command1
command2
command3

y el comando2 falla, mientras imprime un mensaje de error en stderr, entonces parece que ha logrado lo que desea. (¡A menos que malinterprete lo que quieres!)

Como corolario, cualquier comando que escriba debe comportarse bien: debe informar errores a stderr en lugar de stdout (el código de muestra en la pregunta imprime errores a stdout) y debe salir con un estado distinto de cero cuando falla.

Sin embargo, ya no considero que esto sea una buena práctica. set -eha cambiado su semántica con diferentes versiones de bash, y aunque funciona bien para un script simple, hay tantos casos extremos que es esencialmente inutilizable. (Considere cosas como: set -e; foo() { false; echo should not print; } ; foo && echo ok La semántica aquí es algo razonable, pero si refactoriza el código en una función que dependía de la configuración de opción para finalizar anticipadamente, puede ser mordido fácilmente). En mi opinión, es mejor escribir:

 #!/bin/sh

 command1 || exit
 command2 || exit
 command3 || exit

o

#!/bin/sh

command1 && command2 && command3
William Pursell avatar Mar 04 '2011 15:03 William Pursell

Tengo un conjunto de funciones de secuencias de comandos que uso ampliamente en mi sistema Red Hat. Utilizan las funciones del sistema para /etc/init.d/functionsimprimir indicadores de estado verdes [ OK ]y rojos [FAILED].

Opcionalmente, puede establecer la $LOG_STEPSvariable en un nombre de archivo de registro si desea registrar qué comandos fallan.

Uso

step "Installing XFS filesystem tools:"
try rpm -i xfsprogs-*.rpm
next

step "Configuring udev:"
try cp *.rules /etc/udev/rules.d
try udevtrigger
next

step "Adding rc.postsysinit hook:"
try cp rc.postsysinit /etc/rc.d/
try ln -s rc.d/rc.postsysinit /etc/rc.postsysinit
try echo $'\nexec /etc/rc.postsysinit' >> /etc/rc.sysinit
next

Producción

Installing XFS filesystem tools:        [  OK  ]
Configuring udev:                       [FAILED]
Adding rc.postsysinit hook:             [  OK  ]

Código

#!/bin/bash

. /etc/init.d/functions

# Use step(), try(), and next() to perform a series of commands and print
# [  OK  ] or [FAILED] at the end. The step as a whole fails if any individual
# command fails.
#
# Example:
#     step "Remounting / and /boot as read-write:"
#     try mount -o remount,rw /
#     try mount -o remount,rw /boot
#     next
step() {
    echo -n "$@"

    STEP_OK=0
    [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$
}

try() {
    # Check for `-b' argument to run command in the background.
    local BG=

    [[ $1 == -b ]] && { BG=1; shift; }
    [[ $1 == -- ]] && {       shift; }

    # Run the command.
    if [[ -z $BG ]]; then
        "$@"
    else
        "$@" &
    fi

    # Check if command failed and update $STEP_OK if so.
    local EXIT_CODE=$?

    if [[ $EXIT_CODE -ne 0 ]]; then
        STEP_OK=$EXIT_CODE
        [[ -w /tmp ]] && echo $STEP_OK > /tmp/step.$$

        if [[ -n $LOG_STEPS ]]; then
            local FILE=$(readlink -m "${BASH_SOURCE[1]}")
            local LINE=${BASH_LINENO[0]}

            echo "$FILE: line $LINE: Command \`$*' failed with exit code $EXIT_CODE." >> "$LOG_STEPS"
        fi
    fi

    return $EXIT_CODE
}

next() {
    [[ -f /tmp/step.$$ ]] && { STEP_OK=$(< /tmp/step.$$); rm -f /tmp/step.$$; }
    [[ $STEP_OK -eq 0 ]]  && echo_success || echo_failure
    echo

    return $STEP_OK
}
John Kugelman avatar Mar 04 '2011 16:03 John Kugelman

Por si sirve de algo, una forma más corta de escribir código para comprobar el éxito de cada comando es:

command1 || echo "command1 borked it"
command2 || echo "command2 borked it"

Sigue siendo tedioso pero al menos es legible.

John Kugelman avatar Mar 04 '2011 16:03 John Kugelman

Una alternativa es simplemente unir los comandos &&para que el primero que falle impida la ejecución del resto:

command1 &&
  command2 &&
  command3

Esta no es la sintaxis que solicitó en la pregunta, pero es un patrón común para el caso de uso que describe. En general, los comandos deberían ser responsables de los errores de impresión para que no tenga que hacerlo manualmente (tal vez con una -qbandera para silenciar los errores cuando no los desee). Si tiene la capacidad de modificar estos comandos, los editaría para gritar en caso de falla, en lugar de envolverlos en otra cosa que lo haga.


Tenga en cuenta también que no es necesario hacer:

command1
if [ $? -ne 0 ]; then

Puedes simplemente decir:

if ! command1; then

Y cuando necesite verificar los códigos de retorno, use un contexto aritmético en lugar de [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414 avatar Jul 10 '2015 18:07 dimo414