Manejo de errores en Bash

240

¿Cuál es su método favorito para manejar errores en Bash? El mejor ejemplo de manejo de errores que he encontrado en la web fue escrito por William Shotts, Jr en http://www.linuxcommand.org .

Sugiere usar la siguiente función para el manejo de errores en Bash:

#!/bin/bash

# A slicker error handling routine

# I put a variable in my scripts named PROGNAME which
# holds the name of the program being run.  You can get this
# value from the first item on the command line ($0).

# Reference: This was copied from <http://www.linuxcommand.org/wss0150.php>

PROGNAME=$(basename $0)

function error_exit
{

#   ----------------------------------------------------------------
#   Function for exit due to fatal program error
#       Accepts 1 argument:
#           string containing descriptive error message
#   ---------------------------------------------------------------- 

    echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2
    exit 1
}

# Example call of the error_exit function.  Note the inclusion
# of the LINENO environment variable.  It contains the current
# line number.

echo "Example of error with line number and message"
error_exit "$LINENO: An error has occurred."

¿Tiene una mejor rutina de manejo de errores que utiliza en los scripts de Bash?

Noob
fuente
1
Vea esta respuesta detallada: Error de aumento en un script Bash .
codeforester
1
Vea la implementación de registro y manejo de errores aquí: github.com/codeforester/base/blob/master/lib/stdlib.sh
codeforester

Respuestas:

154

¡Usa una trampa!

tempfiles=( )
cleanup() {
  rm -f "${tempfiles[@]}"
}
trap cleanup 0

error() {
  local parent_lineno="$1"
  local message="$2"
  local code="${3:-1}"
  if [[ -n "$message" ]] ; then
    echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}"
  else
    echo "Error on or near line ${parent_lineno}; exiting with status ${code}"
  fi
  exit "${code}"
}
trap 'error ${LINENO}' ERR

... luego, cada vez que crea un archivo temporal:

temp_foo="$(mktemp -t foobar.XXXXXX)"
tempfiles+=( "$temp_foo" )

y $temp_foose eliminará al salir, y se imprimirá el número de línea actual. ( set -etambién le dará un comportamiento de salida en caso de error, aunque viene con advertencias serias y debilita la previsibilidad y portabilidad del código).

Puede dejar que la trampa lo llame error(en cuyo caso usa el código de salida predeterminado de 1 y ningún mensaje) o llamarlo usted mismo y proporcionar valores explícitos; por ejemplo:

error ${LINENO} "the foobar failed" 2

saldrá con el estado 2 y dará un mensaje explícito.

Charles Duffy
fuente
44
@draemon la capitalización variable es intencional. Todo en mayúsculas es convencional solo para las variables de entorno y de construcción de shell: el uso de minúsculas para todo lo demás evita conflictos de espacio de nombres. Ver también stackoverflow.com/questions/673055/…
Charles Duffy
1
antes de romperlo nuevamente, pruebe su cambio. Las convenciones son algo bueno, pero son secundarias al código funcional.
Draemon
3
@Draemon, en realidad no estoy de acuerdo. Obviamente, el código roto se nota y se repara. Las malas prácticas pero el código que funciona en su mayoría vive para siempre (y se propaga).
Charles Duffy
1
Pero no te diste cuenta. El código roto se nota porque el código de funcionamiento es la principal preocupación.
Draemon
55
no es exactamente gratuito ( stackoverflow.com/a/10927223/26334 ) y si el código ya es incompatible con POSIX, eliminar la palabra clave de función no lo hace más capaz de ejecutarse bajo POSIX sh, pero mi punto principal es que usted ' ve (IMO) devaluó la respuesta al debilitar la recomendación de usar set -e. Stackoverflow no se trata de "su" código, se trata de tener las mejores respuestas.
Draemon
123

Esa es una buena solución. Solo quería agregar

set -e

como un mecanismo de error rudimentario. Inmediatamente detendrá su script si falla un comando simple. Creo que este debería haber sido el comportamiento predeterminado: dado que tales errores casi siempre significan algo inesperado, no es realmente "sensato" seguir ejecutando los siguientes comandos.

Bruno De Fraine
fuente
29
set -eno está exento de problemas: consulte mywiki.wooledge.org/BashFAQ/105 para obtener más información.
Charles Duffy
3
@CharlesDuffy, algunas de las trampas pueden superarse conset -o pipefail
hobs
77
@CharlesDuffy Gracias por señalar las trampas; En general, todavía creo que set -etiene una alta relación costo-beneficio.
Bruno De Fraine
3
@BrunoDeFraine Me utilizo a set -emí mismo, pero algunos de los otros clientes habituales en irc.freenode.org # bash aconsejan (en términos bastante fuertes) en contra. Como mínimo, las trampas en cuestión deben ser bien entendidas.
Charles Duffy
3
configurar -e -o pipefail -u # y saber lo que está haciendo
Sam Watkins
78

Leer todas las respuestas en esta página me inspiró mucho.

Entonces, aquí está mi pista:

contenido del archivo: lib.trap.sh

lib_name='trap'
lib_version=20121026

stderr_log="/dev/shm/stderr.log"

#
# TO BE SOURCED ONLY ONCE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

if test "${g_libs[$lib_name]+_}"; then
    return 0
else
    if test ${#g_libs[@]} == 0; then
        declare -A g_libs
    fi
    g_libs[$lib_name]=$lib_version
fi


#
# MAIN CODE:
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
set -o nounset   ## set -u : exit the script if you try to use an uninitialised variable
set -o errexit   ## set -e : exit the script if any statement returns a non-true return value

exec 2>"$stderr_log"


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: EXIT_HANDLER
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function exit_handler ()
{
    local error_code="$?"

    test $error_code == 0 && return;

    #
    # LOCAL VARIABLES:
    # ------------------------------------------------------------------
    #    
    local i=0
    local regex=''
    local mem=''

    local error_file=''
    local error_lineno=''
    local error_message='unknown'

    local lineno=''


    #
    # PRINT THE HEADER:
    # ------------------------------------------------------------------
    #
    # Color the output if it's an interactive terminal
    test -t 1 && tput bold; tput setf 4                                 ## red bold
    echo -e "\n(!) EXIT HANDLER:\n"


    #
    # GETTING LAST ERROR OCCURRED:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    #
    # Read last file from the error log
    # ------------------------------------------------------------------
    #
    if test -f "$stderr_log"
        then
            stderr=$( tail -n 1 "$stderr_log" )
            rm "$stderr_log"
    fi

    #
    # Managing the line to extract information:
    # ------------------------------------------------------------------
    #

    if test -n "$stderr"
        then        
            # Exploding stderr on :
            mem="$IFS"
            local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' )
            IFS=':'
            local stderr_parts=( $shrunk_stderr )
            IFS="$mem"

            # Storing information on the error
            error_file="${stderr_parts[0]}"
            error_lineno="${stderr_parts[1]}"
            error_message=""

            for (( i = 3; i <= ${#stderr_parts[@]}; i++ ))
                do
                    error_message="$error_message "${stderr_parts[$i-1]}": "
            done

            # Removing last ':' (colon character)
            error_message="${error_message%:*}"

            # Trim
            error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
    fi

    #
    # GETTING BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #
    _backtrace=$( backtrace 2 )


    #
    # MANAGING THE OUTPUT:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    local lineno=""
    regex='^([a-z]{1,}) ([0-9]{1,})$'

    if [[ $error_lineno =~ $regex ]]

        # The error line was found on the log
        # (e.g. type 'ff' without quotes wherever)
        # --------------------------------------------------------------
        then
            local row="${BASH_REMATCH[1]}"
            lineno="${BASH_REMATCH[2]}"

            echo -e "FILE:\t\t${error_file}"
            echo -e "${row^^}:\t\t${lineno}\n"

            echo -e "ERROR CODE:\t${error_code}"             
            test -t 1 && tput setf 6                                    ## white yellow
            echo -e "ERROR MESSAGE:\n$error_message"


        else
            regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$"
            if [[ "$_backtrace" =~ $regex ]]

                # The file was found on the log but not the error line
                # (could not reproduce this case so far)
                # ------------------------------------------------------
                then
                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    echo -e "ERROR MESSAGE:\n${stderr}"

                # Neither the error line nor the error file was found on the log
                # (e.g. type 'cp ffd fdf' without quotes wherever)
                # ------------------------------------------------------
                else
                    #
                    # The error file is the first on backtrace list:

                    # Exploding backtrace on newlines
                    mem=$IFS
                    IFS='
                    '
                    #
                    # Substring: I keep only the carriage return
                    # (others needed only for tabbing purpose)
                    IFS=${IFS:0:1}
                    local lines=( $_backtrace )

                    IFS=$mem

                    error_file=""

                    if test -n "${lines[1]}"
                        then
                            array=( ${lines[1]} )

                            for (( i=2; i<${#array[@]}; i++ ))
                                do
                                    error_file="$error_file ${array[$i]}"
                            done

                            # Trim
                            error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )"
                    fi

                    echo -e "FILE:\t\t$error_file"
                    echo -e "ROW:\t\tunknown\n"

                    echo -e "ERROR CODE:\t${error_code}"
                    test -t 1 && tput setf 6                            ## white yellow
                    if test -n "${stderr}"
                        then
                            echo -e "ERROR MESSAGE:\n${stderr}"
                        else
                            echo -e "ERROR MESSAGE:\n${error_message}"
                    fi
            fi
    fi

    #
    # PRINTING THE BACKTRACE:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 7                                            ## white bold
    echo -e "\n$_backtrace\n"

    #
    # EXITING:
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #

    test -t 1 && tput setf 4                                            ## red bold
    echo "Exiting!"

    test -t 1 && tput sgr0 # Reset terminal

    exit "$error_code"
}
trap exit_handler EXIT                                                  # ! ! ! TRAP EXIT ! ! !
trap exit ERR                                                           # ! ! ! TRAP ERR ! ! !


###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
#
# FUNCTION: BACKTRACE
#
###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##

function backtrace
{
    local _start_from_=0

    local params=( "$@" )
    if (( "${#params[@]}" >= "1" ))
        then
            _start_from_="$1"
    fi

    local i=0
    local first=false
    while caller $i > /dev/null
    do
        if test -n "$_start_from_" && (( "$i" + 1   >= "$_start_from_" ))
            then
                if test "$first" == false
                    then
                        echo "BACKTRACE IS:"
                        first=true
                fi
                caller $i
        fi
        let "i=i+1"
    done
}

return 0



Ejemplo de uso:
contenido del archivo: trap-test.sh

#!/bin/bash

source 'lib.trap.sh'

echo "doing something wrong now .."
echo "$foo"

exit 0


Corriendo:

bash trap-test.sh

Salida:

doing something wrong now ..

(!) EXIT HANDLER:

FILE:       trap-test.sh
LINE:       6

ERROR CODE: 1
ERROR MESSAGE:
foo:   unassigned variable

BACKTRACE IS:
1 main trap-test.sh

Exiting!


Como puede ver en la captura de pantalla a continuación, el resultado es de color y el mensaje de error viene en el idioma utilizado.

ingrese la descripción de la imagen aquí

revs Luca Borrione
fuente
3
esto es increíble ... deberías crear un proyecto github para que las personas puedan hacer mejoras y contribuir fácilmente. Lo combiné con log4bash y juntos crea un entorno poderoso para crear buenos scripts de bash.
Dominik Dorn
1
FYI - test ${#g_libs[@]} == 0no es compatible con POSIX (POSIX soportes de prueba =para las comparaciones de cadenas o -eqpara comparaciones numéricas, pero no ==, por no mencionar la falta de matrices en POSIX), y si estás no tratando de ser compatible con POSIX, por qué en el mundo estás utilizando testen lugar de un contexto matemático? (( ${#g_libs[@]} == 0 ))es, después de todo, más fácil de leer.
Charles Duffy
2
@Luca: ¡esto es realmente genial! Su imagen me inspiró a crear mi propia implementación de esto, lo que lo lleva incluso unos pasos más allá. Lo publiqué en mi respuesta a continuación .
niieani
3
Bravissimo !! Esta es una excelente manera de depurar un script. Grazie mille Lo único que agregué fue un cheque para OS X como este: case "$(uname)" in Darwin ) stderr_log="${TMPDIR}stderr.log";; Linux ) stderr_log="/dev/shm/stderr.log";; * ) stderr_log="/dev/shm/stderr.log" ;; esac
SaxDaddy
1
Un autoenchufe un poco descarado, pero tomamos este fragmento, lo limpiamos, agregamos más funciones, mejoramos el formato de salida y lo hicimos más compatible con POSIX (funciona tanto en Linux como en OSX). Se publica como parte de Privex ShellCore en Github: github.com/Privex/shell-core
Someguy123
22

Una alternativa equivalente a "set -e" es

set -o errexit

Hace que el significado de la bandera sea algo más claro que simplemente "-e".

Adición aleatoria: para deshabilitar temporalmente el indicador y volver al valor predeterminado (de ejecución continua independientemente de los códigos de salida), solo use

set +e
echo "commands run here returning non-zero exit codes will not cause the entire script to fail"
echo "false returns 1 as an exit code"
false
set -e

Esto impide el manejo adecuado de errores mencionado en otras respuestas, pero es rápido y efectivo (como bash).

Ben Scholbrock
fuente
1
usar $(foo)en una línea desnuda en lugar de solo fooes generalmente lo incorrecto. ¿Por qué promoverlo dándolo como ejemplo?
Charles Duffy
20

Inspirado por las ideas presentadas aquí, he desarrollado una forma legible y conveniente de manejar los errores en los scripts de bash en mi proyecto bash boilerplate .

Simplemente obteniendo la biblioteca, obtienes lo siguiente de la caja (es decir, detendrá la ejecución ante cualquier error, como si se usara set -egracias a un trapencendido ERRy algo de bash-fu ):

manejo de errores bash-oo-framework

Hay algunas características adicionales que ayudan a manejar los errores, como try and catch , o la palabra clave throw , que le permite interrumpir la ejecución en un punto para ver la traza inversa. Además, si el terminal lo admite, escupe emojis powerline, colorea partes de la salida para una gran legibilidad y subraya el método que causó la excepción en el contexto de la línea de código.

La desventaja es que no es portátil, el código funciona en bash, probablemente solo> = 4 (pero imagino que podría ser portado con algún esfuerzo para bash 3).

El código está separado en varios archivos para un mejor manejo, pero me inspiró la idea de la traza de la respuesta anterior de Luca Borrione .

Para leer más o echar un vistazo a la fuente, vea GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw

revs niieani
fuente
Esto está dentro del proyecto Bash Object Oriented Framework . ... Afortunadamente solo tiene 7.4k LOC (según GLOC ). OOP: ¿dolor orientado a objetos?
ingyhere
@ingyhere es altamente modular (y fácil de eliminar), por lo que solo puede usar la parte de excepciones si es para eso que vino;)
niieani
11

Prefiero algo realmente fácil de llamar. Así que uso algo que parece un poco complicado, pero que es fácil de usar. Por lo general, solo copio y pego el siguiente código en mis scripts. Una explicación sigue el código.

#This function is used to cleanly exit any script. It does this displaying a
# given error message, and exiting with an error code.
function error_exit {
    echo
    echo "$@"
    exit 1
}
#Trap the killer signals so that we can exit with a good message.
trap "error_exit 'Received signal SIGHUP'" SIGHUP
trap "error_exit 'Received signal SIGINT'" SIGINT
trap "error_exit 'Received signal SIGTERM'" SIGTERM

#Alias the function so that it will print a message with the following format:
#prog-name(@line#): message
#We have to explicitly allow aliases, we do this because they make calling the
#function much easier (see example).
shopt -s expand_aliases
alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"'

Normalmente pongo una llamada a la función de limpieza junto a la función error_exit, pero esto varía de un guión a otro, así que lo dejé fuera. Las trampas captan las señales de terminación comunes y se aseguran de que todo se limpie. El alias es lo que hace la verdadera magia. Me gusta verificar todo por fallas. Entonces, en general, llamo a los programas en un "si!" declaración de tipo Al restar 1 del número de línea, el alias me dirá dónde ocurrió la falla. También es muy simple de llamar, y es una prueba bastante idiota. A continuación se muestra un ejemplo (simplemente reemplace / bin / false con lo que sea que vaya a llamar).

#This is an example useage, it will print out
#Error prog-name (@1): Who knew false is false.
if ! /bin/false ; then
    die "Who knew false is false."
fi
Michael Nooner
fuente
2
¿Puede ampliar la afirmación "Tenemos que permitir explícitamente los alias" ? Me preocuparía que pudiera resultar un comportamiento inesperado. ¿Hay alguna manera de lograr lo mismo con un impacto menor?
blong
No necesito $LINENO - 1. Mostrar correctamente sin ella.
kyb
Ejemplo de uso más corto en bash y zshfalse || die "hello death"
kyb
6

Otra consideración es el código de salida para devolver. Simplemente " 1" es bastante estándar, aunque hay un puñado de códigos de salida reservados que bash usa , y esa misma página argumenta que los códigos definidos por el usuario deben estar en el rango 64-113 para cumplir con los estándares C / C ++.

También puede considerar el enfoque de vector de bits que mountutiliza para sus códigos de salida:

 0  success
 1  incorrect invocation or permissions
 2  system error (out of memory, cannot fork, no more loop devices)
 4  internal mount bug or missing nfs support in mount
 8  user interrupt
16  problems writing or locking /etc/mtab
32  mount failure
64  some mount succeeded

ORAl unir los códigos juntos, su script puede señalar múltiples errores simultáneos.

yukondude
fuente
4

Utilizo el siguiente código de captura, también permite rastrear errores a través de tuberías y comandos de 'tiempo'

#!/bin/bash
set -o pipefail  # trace ERR through pipes
set -o errtrace  # trace ERR through 'time command' and other functions
function error() {
    JOB="$0"              # job name
    LASTLINE="$1"         # line of error occurrence
    LASTERR="$2"          # error code
    echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}"
    exit 1
}
trap 'error ${LINENO} ${?}' ERR
Olivier Delrieu
fuente
55
La functionpalabra clave es gratuitamente incompatible con POSIX. Considere hacer su declaración justa error() {, sin functionantes.
Charles Duffy
55
${$?}debería ser $?, o ${?}si insiste en usar aparatos innecesarios; lo interno $está mal.
Charles Duffy
3
@CharlesDuffy por ahora, POSIX es gratis incompatible con GNU / Linux (aún así, entiendo tu punto)
Croad Langshan
3

he usado

die() {
        echo $1
        kill $$
}

antes de; Creo que porque 'salir' me estaba fallando por alguna razón. Sin embargo, los valores predeterminados anteriores parecen una buena idea.

pjz
fuente
Mejor envíe un mensaje de error a STDERR, ¿no?
ankostis
3

Esto me ha servido bien por un tiempo ahora. Imprime mensajes de error o advertencia en rojo, una línea por parámetro, y permite un código de salida opcional.

# Custom errors
EX_UNKNOWN=1

warning()
{
    # Output warning messages
    # Color the output red if it's an interactive terminal
    # @param $1...: Messages

    test -t 1 && tput setf 4

    printf '%s\n' "$@" >&2

    test -t 1 && tput sgr0 # Reset terminal
    true
}

error()
{
    # Output error messages with optional exit code
    # @param $1...: Messages
    # @param $N: Exit code (optional)

    messages=( "$@" )

    # If the last parameter is a number, it's not part of the messages
    last_parameter="${messages[@]: -1}"
    if [[ "$last_parameter" =~ ^[0-9]*$ ]]
    then
        exit_code=$last_parameter
        unset messages[$((${#messages[@]} - 1))]
    fi

    warning "${messages[@]}"

    exit ${exit_code:-$EX_UNKNOWN}
}
l0b0
fuente
3

No estoy seguro de si esto será útil para usted, pero modifiqué algunas de las funciones sugeridas aquí para incluir la verificación del error (código de salida del comando anterior) dentro de él. En cada "verificación" también paso como parámetro el "mensaje" de cuál es el error para fines de registro.

#!/bin/bash

error_exit()
{
    if [ "$?" != "0" ]; then
        log.sh "$1"
        exit 1
    fi
}

Ahora, para llamarlo dentro del mismo script (o en otro si lo uso export -f error_exit), simplemente escribo el nombre de la función y paso un mensaje como parámetro, como este:

#!/bin/bash

cd /home/myuser/afolder
error_exit "Unable to switch to folder"

rm *
error_exit "Unable to delete all files"

Utilizando esto, pude crear un archivo bash realmente robusto para algún proceso automatizado y se detendrá en caso de errores y me notificará ( log.shlo hará)

Nelson Rodriguez
fuente
2
Considere usar la sintaxis POSIX para definir funciones, sin functionpalabras clave, solo error_exit() {.
Charles Duffy
2
¿hay alguna razón por la que no solo lo haces cd /home/myuser/afolder || error_exit "Unable to switch to folder"?
Pierre-Olivier Vares
@ Pierre-OlivierVares No hay una razón particular para no usar ||. Esto fue solo un extracto de un código existente y acabo de agregar las líneas de "manejo de errores" después de cada línea correspondiente. Algunos son muy largos y era más limpio tenerlo en una línea separada (inmediata)
Nelson Rodriguez
Sin embargo, parece una solución limpia, la verificación de shell se queja: github.com/koalaman/shellcheck/wiki/SC2181
mhulse
1

Este truco es útil para los comandos o funciones que faltan. El nombre de la función faltante (o ejecutable) se pasará en $ _

function handle_error {
    status=$?
    last_call=$1

    # 127 is 'command not found'
    (( status != 127 )) && return

    echo "you tried to call $last_call"
    return
}

# Trap errors.
trap 'handle_error "$_"' ERR
Orwellophile
fuente
¿No $_estaría disponible en la función igual que $?? No estoy seguro de que haya alguna razón para usar una en la función, pero no la otra.
ingyhere
1

Esta función me ha estado sirviendo bastante bien recientemente:

action () {
    # Test if the first parameter is non-zero
    # and return straight away if so
    if test $1 -ne 0
    then
        return $1
    fi

    # Discard the control parameter
    # and execute the rest
    shift 1
    "$@"
    local status=$?

    # Test the exit status of the command run
    # and display an error message on failure
    if test ${status} -ne 0
    then
        echo Command \""$@"\" failed >&2
    fi

    return ${status}
}

Lo llama agregando 0 o el último valor de retorno al nombre del comando a ejecutar, de modo que puede encadenar comandos sin tener que verificar los valores de error. Con esto, este bloque de instrucciones:

command1 param1 param2 param3...
command2 param1 param2 param3...
command3 param1 param2 param3...
command4 param1 param2 param3...
command5 param1 param2 param3...
command6 param1 param2 param3...

Se convierte en esto:

action 0 command1 param1 param2 param3...
action $? command2 param1 param2 param3...
action $? command3 param1 param2 param3...
action $? command4 param1 param2 param3...
action $? command5 param1 param2 param3...
action $? command6 param1 param2 param3...

<<<Error-handling code here>>>

Si alguno de los comandos falla, el código de error simplemente se pasa al final del bloque. Me resulta útil cuando no desea que se ejecuten comandos posteriores si falla uno anterior, pero tampoco desea que el script salga de inmediato (por ejemplo, dentro de un bucle).

xarxziux
fuente
0

Usar trap no siempre es una opción. Por ejemplo, si está escribiendo algún tipo de función reutilizable que necesita manejo de errores y que se puede invocar desde cualquier script (después de obtener el archivo con funciones auxiliares), esa función no puede asumir nada sobre el tiempo de salida del script externo, lo que hace que usar trampas sea muy difícil. Otra desventaja del uso de trampas es la mala capacidad de compilación, ya que corre el riesgo de sobrescribir la trampa anterior que podría establecerse antes en la cadena de llamadas.

Hay un pequeño truco que se puede utilizar para manejar los errores de manera adecuada sin trampas. Como ya sabrás por otras respuestas, set -eno funciona dentro de los comandos si usas el ||operador después de ellos, incluso si los ejecutas en una subshell; por ejemplo, esto no funcionaría:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Pero el ||operador es necesario para evitar el regreso de la función externa antes de la limpieza. El truco es ejecutar el comando interno en segundo plano y luego esperarlo inmediatamente. El waitincorporado devolverá el código de salida del comando interno, y ahora está usando ||después wait, no la función interna, por lo que set -efunciona correctamente dentro de este último:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aquí está la función genérica que se basa en esta idea. Debería funcionar en todos los shells compatibles con POSIX si elimina las localpalabras clave, es decir, reemplace todo local x=ycon solo x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Ejemplo de uso:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Ejecutando el ejemplo:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Lo único que debe tener en cuenta al usar este método es que todas las modificaciones de las variables de Shell realizadas desde el comando al que pasa runno se propagarán a la función de llamada, porque el comando se ejecuta en una subshell.

revs sam.kozin
fuente