Скрипт настроен ловить любую ошибку и сообщать о ней. На верхнем уровне это работает: команда падает, ловушка срабатывает, в журнал ложится строка о сбое. Но стоит завернуть ту же падающую команду в функцию, как ловушка немо проглатывает ошибку и ничего не печатает. Скрипт идёт дальше, будто ничего не случилось. Это одно из самых озадачивающих поведений в обработке ошибок bash, и за ним стоит конкретная причина: ловушка ошибок по умолчанию не передаётся внутрь функций.
Механизм обработки ошибок в bash держится на ловушке особого вида - она срабатывает не на сигнал извне, а на провал любой команды в скрипте. В связке с несколькими режимами оболочки она превращает скрипт из того, что слепо продолжает работу после сбоя, в надёжный код, останавливающийся на первой же ошибке с понятным сообщением. Разберём, как настроить эту ловушку, почему она молчит внутри функций, и какой режим возвращает ей голос везде, где он нужен.
Ловушка ERR срабатывает на провал любой команды
Ловушка устанавливается командой установки ловушки, которой указывают, что делать, и тип события - провал команды. С этого момента любая команда, завершившаяся ненулевым кодом, вызовет заданное действие.
trap 'echo "Произошла ошибка в строке $LINENO"' ERR
ls /несуществующий_каталог # провал вызовет ловушку
Особенно ценно, что в действии ловушки доступна переменная номера текущей строки. Она сообщает, на какой строке скрипта случился сбой, что бесценно при поиске причины в длинном коде. Без этого ловушка лишь констатировала бы факт ошибки, а с ней - указывает место.
Сама по себе ловушка ошибок раскрывается в полную силу вместе с режимом немедленного выхода. Этот режим велит оболочке прерывать выполнение при любой команде, завершившейся ненулевым кодом. Без него скрипт после ошибки продолжил бы работу, что обычно ведёт к каскаду новых сбоев на испорченных данных.
set -e # немедленный выход при любой ошибке
trap 'echo "Сбой в строке $LINENO"' ERR
Связка режима выхода и ловушки даёт основу надёжного скрипта: ошибка останавливает выполнение, а ловушка перед остановкой печатает осмысленное сообщение о том, где и что сломалось.
Внутри функции ловушка молчит по умолчанию
Теперь к загадке. Возьмём ловушку, установленную на верхнем уровне, и функцию с заведомо падающей командой.
trap 'echo "Что-то пошло не так"' ERR
проблемная_функция() {
false # эта команда проваливается
}
проблемная_функция
Ожидалось бы увидеть сообщение ловушки. На деле - тишина. Ловушка не сработала, хотя команда внутри функции провалилась. Причина в документированном правиле: ловушка ошибок по умолчанию не наследуется функциями, подстановками команд и командами в подоболочках. На верхнем уровне она ловит ошибки, а стоит выполнению уйти внутрь функции - перестаёт.
Это поведение и сбивает с толку. Скрипт выглядит защищённым, ловушка установлена, а целый класс ошибок - всё, что случается внутри функций, - проскакивает мимо неё незамеченным. Чем больше скрипт опирается на функции, тем больше дыр в такой обработке.
Режим errtrace через set -E возвращает ловушке голос
Лекарство - особый режим оболочки, включаемый флагом заглавной буквы E или равнозначным именем. Он велит наследовать ловушку ошибок внутрь функций, подстановок команд и подоболочек. С ним ловушка работает везде, а не только на верхнем уровне.
set -E # наследовать ловушку ERR в функции и подоболочки
trap 'echo "Что-то пошло не так"' ERR
проблемная_функция() {
false
}
проблемная_функция
# теперь сообщение ловушки печатается
Та же функция с включённым режимом наследования вызовет ловушку как положено. Один флаг разом затыкает все дыры, через которые раньше проскакивали ошибки из функций. Именно поэтому при работе с ловушкой ошибок этот режим советуют включать почти всегда - он делает правила срабатывания простыми и предсказуемыми. Без него правила запутаны: на верхнем уровне ловится, внутри функций нет, в подоболочках по-своему. С ним поведение единообразно везде.
Стандартная связка режимов для надёжного скрипта
На практике сложилась устойчивая комбинация режимов, которую ставят в начало серьёзных скриптов. Каждый флаг закрывает свою брешь, а вместе они дают предсказуемое поведение при ошибках.
set -eEuo pipefail
trap 'echo "Ошибка в строке $LINENO"' ERR
Разберём связку по буквам. Строчная буква выхода прерывает скрипт на первой же ошибке. Заглавная буква наследования передаёт ловушку внутрь функций и подоболочек. Буква запрета неустановленных переменных превращает обращение к необъявленной переменной в ошибку, ловя опечатки в именах. А режим провала конвейера меняет коварное поведение труб: обычно конвейер возвращает код лишь последней команды, и ошибка в середине теряется, а этот режим пропускает наружу первый же сбой в цепочке.
# Без pipefail ошибка в середине конвейера теряется
ложная_команда | sort | uniq # вернёт код uniq, не ложной команды
# С pipefail сбой в любом звене виден
set -o pipefail
ложная_команда | sort | uniq # вернёт ненулевой код
Эта связка из четырёх режимов плюс ловушка - распространённая основа скриптов развёртывания и автоматизации, где тихий сбой особенно опасен. Применённая к реальной функции развёртывания, она ловит и сообщает об ошибке вместо того, чтобы молча продолжить с испорченным состоянием.
Ловушка намеренно пропускает ошибки в проверках и условиях
Есть тонкость, без которой поведение ловушки кажется непоследовательным. Ловушка ошибок намеренно не срабатывает в нескольких случаях, и это не баг, а задумка. Она молчит, если упавшая команда стоит в проверке после ключевого слова условия, в цикле с предусловием, в логической цепочке через И или ИЛИ кроме последнего звена, в любом звене конвейера кроме последнего, или когда код возврата команды намеренно инвертируется.
# Тут ловушка НЕ сработает - и это правильно
if проверить_условие; then ... # провал в проверке не ошибка
команда1 && команда2 # провал команды1 не ошибка
Логика проста: эти конструкции по своей природе проверяют, удалась команда или нет. Провал проверки в условии - не сбой скрипта, а нормальный ход ветвления. Если бы ловушка срабатывала и тут, любая проверка через условие роняла бы скрипт. Те же правила, к слову, действуют и для режима немедленного выхода - он тоже не прерывает скрипт на командах внутри проверок.
Понимание этого спасает от ложного впечатления, будто ловушка работает нестабильно. Она вполне стабильна, просто уважает контексты, где провал команды - часть логики, а не ошибка.
Точечное управление и оговорка про подоболочки
Иногда наследование ловушки нужно лишь вокруг конкретного куска кода, а не на весь скрипт. Режим наследования включают и выключают парными флагами, очерчивая участок, где ловушка должна проникать в функции, а где нет.
set -E # включить наследование
рискованный_участок
set +E # выключить дальше
Стоит держать в уме и особенность подоболочек. Подстановки команд и подоболочки - это форкнутые процессы со своим окружением, и даже с включённым наследованием их поведение бывает с причудами. Режим выхода клонируется в подоболочку, и она тоже прервётся на ошибке, а родитель, увидев её ненулевой код, вызовет свою ловушку. Это работает, но цепочка срабатываний здесь сложнее, чем при прямом вызове, и при отладке стоит помнить, что подоболочка - отдельный процесс со своей копией режимов.
Что складывается в рабочую практику
Картина выстраивается ясно. Ловушка ошибок ловит провал любой команды и через переменную номера строки сообщает, где он случился. Сама по себе она раскрывается вместе с режимом немедленного выхода, который останавливает скрипт на первой ошибке вместо каскада сбоев на испорченных данных.
Ключ к предсказуемости - режим наследования через флаг заглавной E. Без него ловушка молчит внутри функций, и целый класс ошибок проскакивает незамеченным. С ним она работает везде единообразно. Стандартная связка из четырёх режимов плюс ловушка - проверенная основа надёжного скрипта, где каждый флаг закрывает свою брешь: выход на ошибке, наследование ловушки, запрет неустановленных переменных, провал конвейера.
Главная мысль одна: ловушка ошибок честна ровно настолько, насколько широко она наследуется. Загадочное молчание при сбое внутри функции почти всегда означает забытый режим наследования. Стоит поставить связку режимов в начало скрипта и помнить, что ловушка намеренно пропускает ошибки в проверках и условиях, и обработка ошибок перестаёт преподносить сюрпризы. Скрипт ловит сбои ровно там, куда дотягивается его ловушка, а errtrace дотягивает её до всех уголков.