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 -e
no 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 wait
incorporado 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 -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 las local
palabras clave, es decir, reemplace 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 al que pasa run
no se propagarán a la función de llamada, porque el comando se ejecuta en una subshell.