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

261

¿Hay 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 así como:

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

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

jwbensley
fuente
2
Echar un vistazo a la oficial modo estricto fiesta : set -euo pipefail.
Pablo A
1
@PabloBianchi, set -ees una idea horrible . Vea los ejercicios en BashFAQ # 105 discutiendo solo algunos de los casos extremos inesperados que presenta, y / o la comparación que muestra incompatibilidades entre diferentes implementaciones de shells (y versiones de shell) en in-ulm.de/~mascheck/various/set -e .
Charles Duffy

Respuestas:

274

Puede escribir una función que inicie y pruebe el comando por usted. Asume command1y command2son variables de entorno que se han establecido en un comando.

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

mytest "$command1"
mytest "$command2"
krtek
fuente
32
No lo use $*, fallará si algún argumento tiene espacios; utilizar "$@"en su lugar. Del mismo modo, poner $1dentro de las comillas en el echocomando.
Gordon Davisson
82
También evitaría el nombre, testya que es un comando incorporado.
John Kugelman
1
Este es el método con el que fui. Para ser honesto, no creo que haya sido lo suficientemente claro en mi publicación original, pero este método me permite escribir mi propia función de 'prueba' para que luego pueda realizar acciones de error allí que me gustan y que son relevantes para las acciones realizadas en la secuencia de comandos. Gracias :)
jwbensley
77
¿El código de salida devuelto por test () no devolvería siempre 0 en caso de error ya que el último comando ejecutado fue 'echo'? Es posible que deba guardar el valor de $? primero.
magiconair
2
Esta no es una buena idea y fomenta las malas prácticas. Considere el caso simple de ls. Si invoca ls fooy recibe un mensaje de error del formulario ls: foo: No such file or directory\n, comprende el problema. Si, en cambio, ls: foo: No such file or directory\nerror with ls\nte distraes con información superflua. En este caso, es bastante fácil argumentar que la superfluidad es trivial, pero crece rápidamente. Los mensajes de error concisos son importantes. Pero lo más importante, este tipo de envoltorio alienta a los escritores a omitir por completo los buenos mensajes de error.
William Pursell
185

¿Qué quieres decir con "abandonar y repetir el error"? Si quiere decir que desea que la secuencia de comandos finalice tan pronto como falle un comando, simplemente haga

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

al comienzo del guión (pero tenga en cuenta la advertencia a continuación). No se moleste en hacer eco del mensaje de error: deje que el comando que falla 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 command2 falla, al imprimir un mensaje de error en stderr, 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 se basa en la configuración de la opción para terminar antes, 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
fuente
1
Tenga en cuenta que si bien esta solución es la más simple, no le permite realizar ninguna limpieza en caso de falla.
Josh J
66
La limpieza se puede lograr con trampas. (por ejemplo, trap some_func 0se ejecutará some_funca la salida)
William Pursell
3
También tenga en cuenta que la semántica de errexit (set -e) ha cambiado en diferentes versiones de bash, y a menudo se comportará inesperadamente durante la invocación de funciones y otras configuraciones. Ya no recomiendo su uso. En mi opinión, es mejor escribir || exitexplícitamente después de cada comando.
William Pursell
87

Tengo un conjunto de funciones de secuencias de comandos que uso ampliamente en mi sistema Red Hat. Utilizan las funciones del sistema /etc/init.d/functionspara imprimir indicadores de estado verde [ OK ]y rojo [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

Salida

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
fuente
Esto es oro puro. Si bien entiendo cómo usar el script, no entiendo completamente cada paso, definitivamente fuera de mi conocimiento de scripting bash, pero creo que, no obstante, es una obra de arte.
kingmilo
2
¿Esta herramienta tiene un nombre formal? Me encantaría leer una página de manual sobre este estilo de paso / prueba / siguiente registro
ThorSummoner
¿Estas funciones de shell parecen no estar disponibles en Ubuntu? Sin embargo
ThorSummoner
@ThorSummoner, esto es probable porque Ubuntu usa Upstart en lugar de SysV init, y pronto usará systemd. RedHat tiende a mantener la compatibilidad con versiones anteriores por mucho tiempo, razón por la cual las cosas init.d todavía están allí.
dragon788
He publicado una expansión en la solución de John y permite que se use en sistemas que no son de RedHat como Ubuntu. Ver stackoverflow.com/a/54190627/308145
Mark Thomson el
51

Para lo que vale, una forma más corta de escribir código para verificar el éxito de cada comando es:

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

Todavía es tedioso, pero al menos es legible.

John Kugelman
fuente
No creo que esto, no es el método Fui con pero es rápido y fácil de leer, gracias por la información :)
jwbensley
3
Para ejecutar los comandos en silencio y lograr lo mismo:command1 &> /dev/null || echo "command1 borked it"
Matt Byrne
Soy fanático de este método, ¿hay alguna forma de ejecutar múltiples comandos después del OR? Algo así comocommand1 || (echo command1 borked it ; exit)
AndreasKralj
38

Una alternativa es simplemente unir los comandos &&para que el primero en fallar evite que el resto se ejecute:

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 deben 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 necesita hacer:

command1
if [ $? -ne 0 ]; then

Simplemente puedes decir:

if ! command1; then

Y cuando se hace necesario comprobar los códigos de retorno utilizan un contexto en lugar de la aritmética [ ... -ne:

ret=$?
# do something
if (( ret != 0 )); then
dimo414
fuente
34

En lugar de crear funciones de corredor o usar set -e, use un trap:

trap 'echo "error"; do_cleanup failed; exit' ERR
trap 'echo "received signal to stop"; do_cleanup interrupted; exit' SIGQUIT SIGTERM SIGINT

do_cleanup () { rm tempfile; echo "$1 $(date)" >> script_log; }

command1
command2
command3

La trampa incluso tiene acceso al número de línea y a la línea de comando del comando que lo activó. Las variables son $BASH_LINENOy $BASH_COMMAND.

Pausado hasta nuevo aviso.
fuente
44
Si desea imitar un bloque de prueba aún más de cerca, use trap - ERRpara apagar la trampa al final del "bloque".
Gordon Davisson
14

Personalmente, prefiero utilizar un enfoque ligero, como se ve aquí ;

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }
asuser() { sudo su - "$1" -c "${*:2}"; }

Ejemplo de uso:

try apt-fast upgrade -y
try asuser vagrant "echo 'uname -a' >> ~/.profile"
soñoliento
fuente
8
run() {
  $*
  if [ $? -ne 0 ]
  then
    echo "$* failed with exit code $?"
    return 1
  else
    return 0
  fi
}

run command1 && run command2 && run command3
Erik
fuente
66
No ejecute $*, fallará si algún argumento tiene espacios en ellos; utilizar "$@"en su lugar. (Aunque $ * está bien en el echocomando.)
Gordon Davisson
6

Desarrollé una implementación de prueba y captura casi perfecta en bash, que le permite escribir código como:

try 
    echo 'Hello'
    false
    echo 'This will not be displayed'

catch 
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"

¡Incluso puedes anidar los bloques try-catch dentro de ellos!

try {
    echo 'Hello'

    try {
        echo 'Nested Hello'
        false
        echo 'This will not execute'
    } catch {
        echo "Nested Caught (@ $__EXCEPTION_LINE__)"
    }

    false
    echo 'This will not execute too'

} catch {
    echo "Error in $__EXCEPTION_SOURCE__ at line: $__EXCEPTION_LINE__!"
}

El código es parte de mi bash boilerplate / framework . Extiende aún más la idea de probar y atrapar con cosas como el manejo de errores con retroceso y excepciones (además de algunas otras características interesantes).

Aquí está el código que es responsable solo de try & catch:

set -o pipefail
shopt -s expand_aliases
declare -ig __oo__insideTryCatch=0

# if try-catch is nested, then set +e before so the parent handler doesn't catch us
alias try="[[ \$__oo__insideTryCatch -gt 0 ]] && set +e;
           __oo__insideTryCatch+=1; ( set -e;
           trap \"Exception.Capture \${LINENO}; \" ERR;"
alias catch=" ); Exception.Extract \$? || "

Exception.Capture() {
    local script="${BASH_SOURCE[1]#./}"

    if [[ ! -f /tmp/stored_exception_source ]]; then
        echo "$script" > /tmp/stored_exception_source
    fi
    if [[ ! -f /tmp/stored_exception_line ]]; then
        echo "$1" > /tmp/stored_exception_line
    fi
    return 0
}

Exception.Extract() {
    if [[ $__oo__insideTryCatch -gt 1 ]]
    then
        set -e
    fi

    __oo__insideTryCatch+=-1

    __EXCEPTION_CATCH__=( $(Exception.GetLastException) )

    local retVal=$1
    if [[ $retVal -gt 0 ]]
    then
        # BACKWARDS COMPATIBILE WAY:
        # export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-1)]}"
        # export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[(${#__EXCEPTION_CATCH__[@]}-2)]}"
        export __EXCEPTION_SOURCE__="${__EXCEPTION_CATCH__[-1]}"
        export __EXCEPTION_LINE__="${__EXCEPTION_CATCH__[-2]}"
        export __EXCEPTION__="${__EXCEPTION_CATCH__[@]:0:(${#__EXCEPTION_CATCH__[@]} - 2)}"
        return 1 # so that we may continue with a "catch"
    fi
}

Exception.GetLastException() {
    if [[ -f /tmp/stored_exception ]] && [[ -f /tmp/stored_exception_line ]] && [[ -f /tmp/stored_exception_source ]]
    then
        cat /tmp/stored_exception
        cat /tmp/stored_exception_line
        cat /tmp/stored_exception_source
    else
        echo -e " \n${BASH_LINENO[1]}\n${BASH_SOURCE[2]#./}"
    fi

    rm -f /tmp/stored_exception /tmp/stored_exception_line /tmp/stored_exception_source
    return 0
}

Siéntase libre de usar, bifurcar y contribuir: está en GitHub .

niieani
fuente
1
He visto el repositorio y no lo usaré yo mismo, porque es demasiada magia para mi gusto (en mi opinión, es mejor usar Python si uno necesita más poder de abstracción), pero definitivamente un gran +1 de mí porque se ve simplemente increíble.
Alexander Malakhov
Gracias por las amables palabras @AlexanderMalakhov. Estoy de acuerdo con la cantidad de "magia": esa es una de las razones por las que estamos haciendo una lluvia de ideas sobre una versión simplificada 3.0 del marco, que será mucho más fácil de entender, depurar, etc. Hay un problema abierto sobre 3.0 en GH, si querrías incluir tus pensamientos.
niieani
3

Lo siento, no puedo hacer un comentario a la primera respuesta, pero debe usar una nueva instancia para ejecutar el comando: cmd_output = $ ($ @)

#!/bin/bash

function check_exit {
    cmd_output=$($@)
    local status=$?
    echo $status
    if [ $status -ne 0 ]; then
        echo "error with $1" >&2
    fi
    return $status
}

function run_command() {
    exit 1
}

check_exit run_command
desmontar
fuente
2

Para los usuarios de conchas de pescado que tropiezan con este hilo.

Sea foouna función que no "devuelve" (echo) un valor, pero establece el código de salida como de costumbre.
Para evitar verificar $statusdespués de llamar a la función, puede hacer lo siguiente:

foo; and echo success; or echo failure

Y si es demasiado largo para caber en una línea:

foo; and begin
  echo success
end; or begin
  echo failure
end
Dennis
fuente
1

Cuando lo uso ssh, necesito distinguir entre los problemas causados ​​por problemas de conexión y los códigos de error del comando remoto en modo errexit( set -e). Yo uso la siguiente función:

# prepare environment on calling site:

rssh="ssh -o ConnectionTimeout=5 -l root $remote_ip"

function exit255 {
    local flags=$-
    set +e
    "$@"
    local status=$?
    set -$flags
    if [[ $status == 255 ]]
    then
        exit 255
    else
        return $status
    fi
}
export -f exit255

# callee:

set -e
set -o pipefail

[[ $rssh ]]
[[ $remote_ip ]]
[[ $( type -t exit255 ) == "function" ]]

rjournaldir="/var/log/journal"
if exit255 $rssh "[[ ! -d '$rjournaldir/' ]]"
then
    $rssh "mkdir '$rjournaldir/'"
fi
rconf="/etc/systemd/journald.conf"
if [[ $( $rssh "grep '#Storage=auto' '$rconf'" ) ]]
then
    $rssh "sed -i 's/#Storage=auto/Storage=persistent/' '$rconf'"
fi
$rssh systemctl reenable systemd-journald.service
$rssh systemctl is-enabled systemd-journald.service
$rssh systemctl restart systemd-journald.service
sleep 1
$rssh systemctl status systemd-journald.service
$rssh systemctl is-active systemd-journald.service
Tomilov Anatoliy
fuente
1

Puede usar la increíble solución de @john-kugelman que se encuentra arriba en los sistemas que no son RedHat comentando esta línea en su código:

. /etc/init.d/functions

Luego, pegue el siguiente código al final. Divulgación completa: esto es solo una copia directa y pegar los bits relevantes del archivo mencionado anteriormente tomado de Centos 7.

Probado en MacOS y Ubuntu 18.04.


BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"

echo_success() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
    echo -n $"  OK  "
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 0
}

echo_failure() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
    echo -n $"FAILED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_passed() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"PASSED"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
}

echo_warning() {
    [ "$BOOTUP" = "color" ] && $MOVE_TO_COL
    echo -n "["
    [ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
    echo -n $"WARNING"
    [ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
    echo -n "]"
    echo -ne "\r"
    return 1
} 
Mark Thomson
fuente
0

Comprobación del estado de manera funcional

assert_exit_status() {

  lambda() {
    local val_fd=$(echo $@ | tr -d ' ' | cut -d':' -f2)
    local arg=$1
    shift
    shift
    local cmd=$(echo $@ | xargs -E ':')
    local val=$(cat $val_fd)
    eval $arg=$val
    eval $cmd
  }

  local lambda=$1
  shift

  eval $@
  local ret=$?
  $lambda : <(echo $ret)

}

Uso:

assert_exit_status 'lambda status -> [[ $status -ne 0 ]] && echo Status is $status.' lls

Salida

Status is 127
Slavik
fuente