De fato, set -e
não tem efeito dentro de subshells se você usar ||
operator depois deles; por exemplo, isso não funcionaria:
#!/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
Aaron D. Marasco em sua resposta faz um ótimo trabalho de explicar por que ele se comporta dessa maneira.
Aqui está um pequeno truque que pode ser usado para corrigir isso: execute o comando interno em segundo plano e espere imediatamente por ele. O wait
builtin retornará o código de saída do comando interno, e agora você está usando ||
após wait
, não a função interna, portanto set -e
funciona corretamente dentro do ú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
Aqui está a função genérica que se baseia nessa ideia. Ele deve funcionar em todos os shells compatíveis com POSIX se você remover local
keywords, ou seja, substituir todos os local x=y
por apenas 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
}
Exemplo 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 "$@"
Executando o exemplo:
$ ./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
A única coisa que você precisa estar ciente ao usar este método é que todas as modificações de variáveis do shell feitas a partir do comando que você passar para run
não se propagará para a função de chamada, porque o comando é executado em uma subcaixa.