Использование ловушки не всегда вариант. Например, если вы пишете какую-то повторно используемую функцию, которая требует обработки ошибок и которая может быть вызвана из любого скрипта (после получения файла вспомогательными функциями), эта функция не может предполагать время выхода внешнего скрипта, что делает использование ловушек очень сложным. Другим недостатком использования ловушек является плохая компоновка, так как вы рискуете перезаписать предыдущую ловушку, которая может быть установлена ранее в цепочке вызывающих абонентов.
Есть небольшая хитрость, которую можно использовать для правильной обработки ошибок без ловушек. Как вы, возможно, уже знаете из других ответов, set -e
не работает внутри команд, если вы используете оператор ||
после них, даже если вы запускаете их в подоболочке; например, это не сработает:
#!/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
Но оператор ||
необходим для предотвращения возврата из внешней функции перед очисткой. Хитрость заключается в том, чтобы запустить внутреннюю команду в фоновом режиме, а затем сразу же ждать ее. Встроенный wait
вернет код выхода внутренней команды, и теперь вы используете ||
после wait
, а не внутреннюю функцию, поэтому set -e
правильно работает внутри последней:
#!/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
Вот общая функция, основанная на этой идее. Он должен работать во всех POSIX-совместимых оболочках, если вы удалите ключевые слова local
, то есть замените все local x=y
на 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
}
Пример использования:
#!/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 "$@"
Запуск примера:
$ ./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
Единственное, что вам необходимо знать при использовании этого метода, это то, что все изменения переменных оболочки, сделанные из команды, которую вы передаете run
, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.