Почему 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, добавление удобного сообщения снаружи перестаёт менять смысл внутренних шагов.

Ещё по теме

Продолжить изучение

Хотите перейти от отдельных команд к сопровождению Linux-серверов? Работа с системой, скриптами и сервисами входит в программу «Linux и Bash для DevOps-инженеров».

Продолжить на практике

  • Linux и Bash для DevOps-инженеров ↗

    Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.