Вопрос или проблема
Чтобы захватить stdout и код выхода, когда флаг -e
(выйти из оболочки немедленно, если команда завершилась неудачно) не установлен, я бы использовал
OUTPUT="$(my_command)"
exit_code=$?
Чтобы захватить код выхода при установленном -e
, я бы использовал
exit_code=0
my_command || exit_code=$?
Но комбинировать два подхода не получится:
exit_code=0
OUTPUT="$(my_command || exit_code=$?)"
Это не установит exit_code
, потому что он присваивается в подсистеме, и копия в родительской оболочке не обновляется.
Так как же мне захватить stdout и код выхода? Я мог бы отключить -e
или, например, записать код выхода во временный файл, но я хотел бы что-то проще.
Я, должно быть, допустил ошибку, когда пытался решить эту проблему. Решение очевидно. Тем не менее, я не смог найти ответ с помощью своей поисковой системы, поэтому решил добавить этот вопрос здесь.
Решение:
exit_code=0
OUTPUT="$(my_command)" || exit_code=$?
то есть, переместите || exit_code=$?
за пределы подсистемы.
Обратите внимание: не добавляйте перед этим export
или local
, так как это перезапишет код выхода my_command
. Устанавливать переменные как local
или export
можно в отдельной команде.
Это сводится к как отключить эффект errexit
для одной команды, здесь для присваивания переменной.
errexit
отменяется, когда команда участвует в условии, как в
if cmd; then...
(то же самое сwhile
/until
; то же вif cmd; other-cmd; then...
)! cmd
cmd || ...
cmd && ...
В output=$(cmd)
у вас две команды:
- простая команда присвоения переменной
- внутри подсистемы, вывод которой захватывается для создания значения этого присваивания, еще одна простая команда
cmd
:
При включенном errexit
, если cmd
завершается неудачно, подсистема выходит (с тем же статусом выхода), например, вы обнаружите, что в var=$(false; echo здесь>&2)
, echo здесь
не выполняется; статус выхода присваивания переменной будет статусом этой подсистемы, поэтому также неудачным, и процесс оболочки, интерпретирующий это присваивание, тоже выйдет.
Если это присваивание является частью условия, то errexit
отключается рекурсивно во всем коде оболочки, который может быть интерпретирован там.
В:
if
! output=$(cmd)
then
echo>&2 Это неудача
fi
errexit
отключен как для присваивания переменной, так и для cmd
, и если cmd
сама является функцией, например, для каждой команды, которую эта функция выполняет, даже в подсистемах, даже если эта функция явно вызывает set -o errexit
/ set -e
сама.
Здесь, если вы хотите обработать сбой cmd
, независимо от того, включен errexit
или нет, вы можете использовать если ! output=$(cmd); затем
выше или:
output=$(cmd) || {
echo>&2 Обработка сбоя
exit 1
}
Если вам необходимо получить фактическое значение статуса выхода в обработчике, вы можете сделать:
if
output=$(cmd)
exit_status=$?
[ "$exit_status" -ne 0 ]
then
echo>&2 "Это неудача со статусом $exit_status"
...
fi
Если вы просто хотите записать статус выхода пока, возможно для обработки его позже, канонический способ просто отменить errexit
для одной команды (с оговоркой, как указано выше, что это применяется рекурсивно), заключается в добавлении && true
.
cmd || true
Это означает, что вы хотите полностью игнорировать любые сбои cmd
. Не только errexit
отменяется, но и общий статус выхода будет отражать успех. В то время как:
cmd && true
Отменяет errexit
, но также сохраняет статус выхода cmd
в статусе выхода всего списка и/или. Итак:
output=$(cmd) && true
exit_status=$?
other_commands
if [ "$exit_status" -ne 0 ]; then
echo>&2 "О, кстати, cmd неудачно завершился ранее со статусом $exit_status"
fi
Если код был output=$(cmd1; cmd2)
, вы, вероятно, захотите изменить его на output=$(cmd1 && cmd2)
, потому что, с отмененным errexit
, cmd2
в противном случае будет выполнен даже если cmd1
завершился неудачно.
Или вы можете совсем отказаться от errexit
как это делают и рекомендуют многие люди, и выполнить правильную обработку ошибок во всем вашем скрипте. Это хорошее упражнение — остановиться и подумать, что должно произойти, если команда завершится неудачно, и решить, что делать в этом случае, а не надеяться, что errexit
сделает правильную вещь, что на практике редко происходит последовательно, за исключением самых простых скриптов.
Один из возможных способов — использовать команду вроде этой:
OUTPUT="$(my_command)" ; exit_code=$?
С этим вы избегаете одной строки и напрямую присваиваете код выхода
Если установлен -e
, вы можете временно отключить его, выполнить команду и затем установить его снова. Здесь вы можете найти пример скрипта для этого.
Ответ или решение
Вопрос о том, как захватить стандартный вывод (stdout) и код завершения команды в Bash при активном флаге -e
(который заставляет оболочку немедленно выходить при неудаче команды) является не менее важным, чем сложным. В данном ответе мы рассмотрим подходящие методы для достижения этой цели, без необходимости отключать флаг -e
или использовать временные файлы.
Проблема
Флаг -e
является важным инструментом в Bash-скриптах, поскольку он позволяет обнаруживать и реагировать на ошибки. Однако его использование затрудняет захват кода завершения при выполнении команд, поскольку при возникновении ошибки, выполнение скрипта прерывается. В стандартной форме присвоения переменной, такой как:
OUTPUT="$(my_command)"
exit_code=$?
Этот код не сработает, если my_command
завершится с ошибкой, потому что выполнение остановится до того, как будет выполнена операция получения кода завершения.
Решение
Проблему можно решить, переместив проверку кода завершения за пределы подсчета (subshell):
exit_code=0
OUTPUT="$(my_command)" || exit_code=$?
Этот метод позволяет сначала присвоить значение переменной OUTPUT
, а затем, в случае неудачи, установить переменную exit_code
.
Дополнительные нюансы
-
Не используйте
export
илиlocal
: Если вы используете ключевое словоlocal
илиexport
, это может перезаписать значение exit-кодаmy_command
, что приведет к нежелательным последствиям. Важно сохранять переменнуюexit_code
как локальную для вашей логики. -
Отключение
errexit
для одной команды:errexit
отменяется при использовании условия, например:if ! OUTPUT="$(my_command)"; then echo "Команда завершилась неудачно" fi
В этом случае
errexit
будет временно отключен, и вы сможете обрабатывать ошибки без выхода из скрипта. -
Работа с кодами завершения: Если вам нужно захватить код завершения для дальнейшей обработки, можно использовать следующий пример:
if OUTPUT="$(my_command)"; then exit_status=0 else exit_status=$? echo "Ошибка выполнения команды с кодом: $exit_status" fi
-
Игнорирование ошибок: Использование конструкций
cmd || true
позволяет игнорировать ошибки, однако чаще всего лучше обрабатывать ошибки и принимать соответствующие меры.
Заключение
Работа с флагом -e
и одновременно с получением кода завершения команды может быть непростой задачей в Bash. Однако, с помощью предложенных методов, можно эффективно управлять ошибками и захватывать необходимые значения, не теряя в гибкости скрипта. Эти примеры не только помогут вам лучше понять использование Bash, но и улучшат качество ваших скриптов, обеспечивая более надежное и функциональное взаимодействие с командной строкой.