L'uso di trap non è sempre un'opzione. Ad esempio, se stai scrivendo una sorta di funzione riutilizzabile che richiede la gestione degli errori e che può essere richiamata da qualsiasi script (dopo aver acquisito il file con le funzioni di supporto), tale funzione non può assumere nulla sul tempo di uscita dello script esterno, che rende molto difficile l'uso delle trappole. Un altro svantaggio dell'uso delle trap è la scarsa componibilità, poiché si rischia di sovrascrivere le trap precedenti che potrebbero essere impostate in precedenza nella catena del chiamante.
C'è un piccolo trucco che può essere usato per gestire correttamente gli errori senza trappole. Come forse già saprai da altre risposte, set -e
non funziona all'interno dei comandi se usi l' ||
operatore dopo di loro, anche se li esegui in una subshell; ad esempio, questo non funzionerebbe:
#!/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
Ma l' ||
operatore è necessario per impedire il ritorno dalla funzione esterna prima della pulizia. Il trucco è eseguire il comando interno in background, quindi attendere immediatamente. Il wait
builtin restituirà il codice di uscita del comando interno, e ora stai usando ||
dopo wait
, non la funzione interna, quindi set -e
funziona correttamente all'interno di quest'ultimo:
#!/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
Ecco la funzione generica che si basa su questa idea. Dovrebbe funzionare in tutte le shell compatibili con POSIX se rimuovete le local
parole chiave, ovvero sostituite tutte 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
}
Esempio di utilizzo:
#!/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 "$@"
Eseguendo l'esempio:
$ ./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
L'unica cosa di cui è necessario essere consapevoli quando si utilizza questo metodo è che tutte le modifiche delle variabili Shell effettuate dal comando a cui si passa run
non si propagheranno alla funzione chiamante, poiché il comando viene eseguito in una subshell.