Manejo de errores en Bash

¿Cuál es tu 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 .

Él sugiere utilizar 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  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 usa en scripts Bash?

    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 

    … entonces, cada vez que creas un archivo temporal:

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

    y $temp_foo se eliminarán al salir, y se imprimirá el número de línea actual. ( set -e le dará también un comportamiento de salida en caso de error, aunque con salvedades serias y debilita la capacidad de predicción y portabilidad del código).

    Puede dejar que la trampa lo llame por 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.

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

     set -e 

    como un mecanismo de error rudimentario. Inmediatamente detendrá tu 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.

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

    Entonces, aquí está mi sugerencia:

    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='^([az]{1,}) ([0-9]{1,})$' if [[ $error_lineno =~ $regex ]] # The error line was found on the log # (eg 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 # (eg 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 siguiente captura de pantalla, la salida está coloreada y el mensaje de error aparece en el idioma usado.

    enter image description here

    Una alternativa equivalente a “set -e” es

     set -o errexit 

    Hace que el significado de la bandera sea algo más claro que solo “-e”.

    Adición aleatoria: para deshabilitar temporalmente el indicador y regresar 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 correcto de errores mencionado en otras respuestas, pero es rápido y efectivo (al igual que bash).

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

    Simplemente al buscar la biblioteca, obtendrá lo siguiente de la caja (es decir, detendrá la ejecución de cualquier error, como si usara set -e gracias a una trap en ERR y 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 de la línea eléctrica, 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> = 4 solamente (pero me imagino que podría ser portado con un poco de esfuerzo para bash 3).

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

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

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

    Prefiero algo realmente fácil de llamar. Así que utilizo algo que parece un poco complicado, pero es fácil de usar. Normalmente solo copio y pego el código a continuación en mis scripts. Una explicación sigue al 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 en el lado de la función error_exit, pero esto varía de un script a otro, así que lo dejé fuera. Las trampas detectan las señales de terminación comunes y se aseguran de que todo se limpia. El alias es lo que hace la verdadera magia. Me gusta comprobar todo por el fracaso. Entonces, en general, llamo a los progtwigs en un “si!” tipo statement. Al restar 1 del número de línea, el alias me dirá dónde ocurrió la falla. También es muy fácil de llamar, y es una prueba bastante idiota. A continuación se muestra un ejemplo (simplemente reemplaza / bin / falso con lo que sea que vayas 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 

    Otra consideración es el código de salida para devolver. Solo ” 1 ” es bastante estándar, aunque hay un puñado de códigos de salida reservados que usa bash , 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 usa mount 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 

    OR al combinar los códigos, tu secuencia de comandos puede señalar múltiples errores simultáneos.

    he usado

     die() { echo $1 kill $$ } 

    antes de; Creo que porque ‘exit’ me falló por alguna razón. Los valores predeterminados anteriores parecen una buena idea, sin embargo.

    Esto me ha servido bien desde hace un tiempo. 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} } 

    Utilizo el siguiente código de trampa, 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 

    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 “comprobació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 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" 

    Usando esto, pude crear un archivo bash realmente robusto para algunos procesos automáticos y se detendrá en caso de errores y me notificará ( log.sh lo hará)

    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} } 

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

     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... < <>> 

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

    Este truco es útil para perder comandos o funciones. 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 

    Usar trampa 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 puede invocarse desde cualquier script (después de abastecer 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 comstackción, ya que corre el riesgo de sobreescribir una trampa anterior que podría establecerse antes en la cadena de llamantes.

    Hay un pequeño truco que se puede usar para hacer un correcto manejo de errores sin trampas. Como ya sabrá por otras respuestas, set -e no funciona dentro de comandos si usa || operador después de ellos, incluso si los ejecuta en una subcapa; 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 regresar de la función externa antes de la limpieza. El truco es ejecutar el comando interno en segundo plano, y luego esperarlo de inmediato. La orden interna de wait devolverá el código de salida del comando interno, y ahora estás usando || después de wait , no la función interna, entonces set -e funciona 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 local palabras clave local , es decir, reemplaza todo local x=y con 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 que pasa a run no se propagarán a la función de llamada, porque el comando se ejecuta en una subcadena.