Почему set -e не остановил Bash-функцию внутри if
В скрипте включён set -e. Функция выполняет несколько шагов, поэтому автор рассчитывает на немедленное завершение при первой ошибке. Позже вызов оборачивают в if, чтобы напечатать удобное сообщение. После этой небольшой правки функция неожиданно продолжает действия после сбоя и попадает в ветку успеха.
Проблема связана с контекстом вызова. Bash не применяет errexit одинаково ко всем ненулевым результатам. Условие if специально должно допускать неуспех: по нему shell выбирает ветку. Когда в этом контексте вызывается функция, игнорирование -e распространяется и на команды её тела.
Увидим разницу на минимальном примере
#!/usr/bin/env bash
set -e
prepare() {
false
printf 'next step\n'
}
if prepare; then
printf 'reported success\n'
else
printf 'reported failure\n'
fi
Здесь false завершается с кодом 1, но затем выполняется printf. Он успешен, поэтому статус всей функции равен нулю и выводится reported success. Это следствие правил Bash, а не случайная потеря кода одной утилитой. Включение set -e ещё раз внутри prepare не меняет условный контекст.
Если вызвать эту же функцию отдельной командой в подходящем обычном контексте, errexit способен завершить shell на false. Поэтому функция, протестированная только прямым вызовом, может вести себя иначе после добавления обработки результата через if или список с ||.
Определим, где функция должна остановиться
Функция должна явно вернуть неуспех критичного шага. Ниже первый шаг моделируется отдельной функцией с кодом 7; после его сбоя полезная работа продолжаться не должна:
#!/usr/bin/env bash
set -e
fetch_config() { return 7; }
prepare() {
fetch_config || return "$?"
printf 'next step\n'
}
if prepare; then
printf 'reported success\n'
else
rc=$?
printf 'reported failure: %s\n' "$rc"
fi
Ожидается только reported failure: 7. Код сохраняется прямо в return, пока его не перезаписала другая команда. В настоящей функции такая граница ставится после каждого шага, без которого следующие действия теряют смысл: получения входа, проверки конфигурации, построения результата.
Если требуется журналирование, сохраните код в локальную переменную сразу после неудачной команды, затем пишите сообщение и возвращайте сохранённое значение. Сам printf тоже имеет статус; после него $? уже относится к нему. Для общего библиотечного кода удобнее return, чтобы вызывающий скрипт мог выбрать действие; exit завершает shell целиком.
Не потеряем код при инверсии
Запись if ! prepare; then rc=$?; ... выглядит естественно, но в этой ветке $? — результат инверсии через !. Он уже равен нулю. Если нужен исходный код, используйте обычный if с else, как в примере. Это особенно важно, когда вызывающий различает временную недоступность, ошибку конфигурации и отсутствие данных.
Явная обработка не означает, что любой ненулевой результат одинаково аварийный. У некоторых программ отдельный код описывает ожидаемое состояние. Сначала прочитайте контракт команды, затем решите, нужно ли возвращать ошибку или продолжить по другой ветке. Универсальное || true уничтожает это различие.
Проверим функцию в её реальном месте вызова
Для функции с побочными действиями полезны три входа: успешный первый шаг, ожидаемый отрицательный ответ и настоящая ошибка. Убедитесь, что последующий шаг выполняется только в нужных случаях. Тестируйте функцию именно через тот if или список, который использует рабочий скрипт.
Опции set -e, pipefail и обработчики ERR помогают организовать скрипт, но не заменяют контракт между функциями. Ошибка конвейера — отдельный вопрос; в этой ситуации даже одиночная false продолжает выполнение из-за контекста вызова. Когда критические границы выражены через явный return, добавление удобного сообщения снаружи перестаёт менять смысл внутренних шагов.
Ещё по теме
- EnvironmentFile в systemd: почему переменная работает в терминале, но не в сервисе
- Bash-скрипт сообщил об успехе после ошибки: код возврата и pipefail
Продолжить изучение
Хотите перейти от отдельных команд к сопровождению Linux-серверов? Работа с системой, скриптами и сервисами входит в программу «Linux и Bash для DevOps-инженеров».
Продолжить на практике
- Linux и Bash для DevOps-инженеров ↗
Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.
