Обработка ошибок в Bash: set -e, -u, pipefail и отладка
Быстрые ссылки
- Bash-скрипты и условия ошибок
- Демонстрация проблемы
- Опция set -e
- Ошибки в пайпах
- Поимка неинициализированных переменных
- Дополнительные приёмы и чек-листы

В Bash скриптах поведение при ошибках управляется набором опций, задаваемых командой set. Правильная комбинация опций помогает остановить выполнение при ошибках, корректно возвращать код завершения и упростить отладку. В этой статье объясняются основные опции, даются примеры, типичные подводные камни и практический чек-лист для надёжных скриптов.
Bash-скрипты и условия ошибок
Bash-скрипты удобны и быстры для написания. Они вызывают стандартные утилиты Linux, потому скрипт может зависеть от внешних процессов. Когда внешняя команда завершается с ненулевым кодом, Bash по умолчанию не прерывает весь скрипт — выполнение продолжается. Это может привести к цепочке ошибок или к ложному нулевому коду завершения самого скрипта, если следующая команда вернёт 0.
Простейший способ избежать таких сценариев — явно обрабатывать код завершения каждой команды. Но это неудобно, особенно при длинных пайпах. Набор опций set позволяет задать желаемое поведение глобально.
Опции, о которых пойдёт речь:
- set -e (errexit): выйти при ненулевом коде
- set -u (nounset): ошибка при обращении к неинициализированной переменной
- set -o pipefail: ненулевой код любого процесса в пайпе делает пайп ненулевым
- set -x (xtrace): печать команд и аргументов во время выполнения (отладка)
Ниже — пошаговая демонстрация и практические рекомендации.
Демонстрация проблемы
Вот простой скрипт, который выводит две строки. Сохраните как script-1.sh.
#!/bin/bash
echo This will happen first
echo This will happen secondСделайте его исполняемым:
chmod +x script-1.shЗапуск покажет обе строки.
Если изменить скрипт и вызвать несуществующий файл через ls, скрипт продолжит выполнение, даже если ls вернул ошибку. Пример script-2.sh:
#!/bin/bash
echo This will happen first
ls imaginary-filename
echo This will happen secondПри запуске вы увидите сообщение об ошибке от ls, но скрипт всё равно выполнит второй echo. Переменная “$?” после завершения скрипта будет содержать код последней команды (в нашем примере — код второго echo), то есть 0. Это даёт ложное ощущение успеха при вызове скрипта извне.

Опция set -e
Опция set -e останавливает выполнение скрипта при любой команде, вернувшей ненулевой код. Простое изменение позволяет скрипту завершиться при ошибке:
#!/bin/bash
set -e
echo This will happen first
ls imaginary-filename
echo This will happen secondПри запуске такой скрипт остановится на failed ls, и код возврата скрипта будет ненулевым — корректно отражая неуспех.

Важно: set -e не всегда срабатывает во всех контекстах — см. раздел «Когда set -e не срабатывает» ниже.
Ошибки в пайпах
Пайпы (|) усложняют ситуацию: по умолчанию код возврата пайпа — это код последней команды в цепочке. Если ошибка произошла в середине пайпа, но последняя команда вернула 0, то ошибка будет «потеряна».
Для демонстрации используем встроенные true и false, которые возвращают 0 и 1 соответственно:
true
echo $?
false
echo $?Если выполнить:
false | true
echo $?то результат будет 0 — потому что true стоит последним.
Bash предоставляет массив PIPESTATUS, который содержит коды завершения всех процессов в последнем пайпе:
false | true | false | true
echo "${PIPESTATUS[0]} ${PIPESTATUS[1]} ${PIPESTATUS[2]} ${PIPESTATUS[3]}"Но вручную разбирать PIPESTATUS неудобно. Для того, чтобы сделать пайп «чувствительным» к ошибкам любых команд внутри него, используйте опцию pipefail:
#!/bin/bash
set -e
echo This will happen first
cat script-99.sh | wc -l
echo This will happen secondЭтот скрипт завершится с кодом 0 по причине того, что wc вернул 0. Если добавить опцию pipefail:
#!/bin/bash
set -eo pipefail
echo This will happen first
cat script-99.sh | wc -l
echo This will happen secondто скрипт завершится с ошибкой, если любая команда в пайпе вернула ненулевой код.


Поимка неинициализированных переменных
Обращение к неинициализированной переменной в Bash по умолчанию возвращает пустую строку, и скрипт продолжает выполнение, что может привести к скрытым ошибкам.
Рассмотрим пример:
#!/bin/bash
set -eo pipefail
echo "$notset"
echo "Another echo command"При выполнении этот скрипт просто выведет пустую строку и продолжит работу. Код возврата будет 0.

Чтобы ловить такие ситуации, добавьте set -u. Тогда обращение к несуществующей переменной приведёт к аварийному завершению:
#!/bin/bash
set -eou pipefail
echo "$notset"
echo "Another echo command"Этот скрипт остановится и вернёт ненулевой код.
Опция -u не срабатывает в местах, где вы явно обрабатываете пустые значения, если использовать синтаксис подстановки по умолчанию, например ${VAR:-} или ${VAR:-default}. Поэтому в проверках переменных часто применяют конструкцию:
if [ -z "${New_Var:-}" ]; then
echo "New_Var has no value assigned to it."
fiили задают дефолтное значение:
default_value=484
Value=${New_Var:-$default_value}
echo "New_Var=$Value"Дополнительные опции: отладка и трассировка
Опция set -x включает трассировку: каждая команда и её аргументы печатаются перед выполнением. Это простой и полезный инструмент для отладки сложных скриптов.
#!/bin/bash
set -euxo pipefail
if [ -z "${New_Var:-}" ]; then
echo "New_Var has no value assigned to it."
fiset -x стоит включать локально или запускать скрипт с -x в окружении CI только для диагностических прогонов, чтобы не засорять лог в нормальных условиях.
Когда set -e не срабатывает
Важно знать случаи, когда set -e (errexit) не приводит к выходу:
- Команды внутри условных выражений if, while, until. Если команда используется как часть выражения if cmd; then …, и cmd вернёт ненулевой код, то errexit не будет срабатывать, потому что результат оценивается явно.
- Внутри && и || (логические комбинации). Ошибки в левой части конструкции cmd1 && cmd2 не всегда убьют весь скрипт.
- В подшеллах, исполненных через ( … ), поведение может отличаться.
- В командах, где ошибку ожидают и обрабатывают (например, проверка существования файла с || true).
Примеры:
if grep -q pattern file; then
echo "found"
fiЕсли grep вернёт 2 (файл недоступен), то поведение зависит от контекста. Поэтому не полагайтесь на set -e как на единственный механизм контроля — используйте явные проверки там, где важно точное поведение.
Альтернативные и дополнительные приёмы
- Используйте trap ERR для централизованной обработки ошибок:
trap 'echo "Ошибка на строке $LINENO" >&2; exit 1' ERR- Для критичных команд применяйте явную проверку кода возврата:
cmd || { echo "cmd failed" >&2; exit 1; }Иногда удобнее оборачивать логические блоки в функции и проверять результат функции.
Для пайпов можно явно проверить PIPESTATUS сразу после выполнения пайпа:
cmd1 | cmd2
ps=(${PIPESTATUS[@]})
if [ "${ps[0]}" -ne 0 ]; then
echo "cmd1 failed"
exit 1
fi- В CI/CD используйте минимальный набор опций: set -euo pipefail и вывод трассировки только при падениях.
Практическое руководство по жёсткой защите скриптов (мини-методология)
- В начале каждого скрипта добавляйте:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'- Инициализируйте переменные явно.
- Используйте ${var:-default} там, где допускается дефолт.
- Оборачивайте операции с внешним вводом в проверки существования файлов и прав доступа.
- Добавьте trap для централизованного логирования ошибок и завершения.
- Ограничьте применение set -x — включайте его при отладке.
Чек-лист ролей
Разработчик:
- Добавил set -euo pipefail в шаблон скрипта.
- Инициализировал все важные переменные.
- Написал тесты для сценариев отказа.
- Добавил комментарии и документацию.
Оператор (DevOps):
- В CI запускается линтер shellcheck.
- В CI/альтернативном окружении включён режим трассировки для упавших прогонов.
- Резервное копирование конфигураций перед выполнением изменений скрипта.
Тестер:
- Проверил скрипт при отсутствии файлов.
- Проверил поведение в пайпах.
- Проверил поведение при некорректных входных данных.
Чек-лист событий и runbook при ошибке
- При неожиданном завершении: посмотреть лог, строчку с LINENO из trap.
- Повторить выполнение с set -x для диагностики.
- Проверить права доступа и наличие входных файлов.
- Восстановить состояние, если скрипт частично выполнил изменения.
Сниппет-«шпаргалка» по опциям set
# Надёжный старт скрипта
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Включить трассировку локально
set -x
# Выключить трассировку
set +xКогда нужно отключать errexit намеренно
Иногда нужно обрабатывать ошибку локально и не завершать весь скрипт. В таких случаях:
- Временное отключение errexit:
set +e
command_may_fail
rc=$?
set -e
if [ $rc -ne 0 ]; then
echo "Handled failure"
fi- Использование конструкции || true там, где ошибка ожидаема и допустима.
Test cases и критерии приёмки
Критерии приёмки:
- Скрипт возвращает ненулевой код при отсутствии требуемого файла.
- Скрипт возвращает ненулевой код при ошибке внутри пайпа.
- Скрипт прерывается при обращении к неинициализированной переменной (если не указан дефолт).
- При включённой трассировке (set -x) видно последовательность команд.
Тесты (примеры):
- Запустить скрипт с несуществующим входным файлом — ожидается код != 0.
- Запустить скрипт с пайпом, где первая команда возвращает 1 — при set -o pipefail ожидается код != 0.
- Запустить скрипт с неинициализированной переменной — при set -u ожидается аварийное завершение.
Психологическая модель и эвристики
- Модель: каждая команда — независимый шаг; если предыдущий шаг критичен для следующего, его ошибка должна остановить процесс.
- Эвристики:
- Применяйте set -e там, где каждая команда критична.
- Используйте явную проверку, если ожидается «контрабандная» обработка ошибок.
- Для библиотечных функций предпочитайте возвращать коды и обрабатывать их явно.
Примеры типичных ошибок и как их избежать
Ошибка: команда в if-условии приводит к аварии, когда этого не ждут. Решение: явно обработайте код или используйте || true в условии.
Ошибка: PIPESTATUS перезаписывается следующей командой. Решение: записывайте PIPESTATUS сразу после пайпа в локальную переменную.
Ошибка: set -u ломает участки, где допускается пустой ввод. Решение: пользуйтесь ${var:-} при проверках.
Решение для CI и production
- В CI: включайте set -euo pipefail и строгую валидацию входных параметров. Логи храните и включайте трассировку только для упавших прогонов.
- В production: предоставьте механизм «dry run» и пост-условные проверки целостности системы после выполнения скрипта.
Пример decision tree (Mermaid)
flowchart TD
A[Начало] --> B{Есть входные файлы?}
B -- Нет --> C[Вывести ошибку и exit 1]
B -- Да --> D{Команда критична?}
D -- Да --> E[Выполнить с явной проверкой rc || exit 1]
D -- Нет --> F[Выполнить и логировать результат]
E --> G[Следующий шаг]
F --> G
G --> H{Используется пайп?}
H -- Да --> I[set -o pipefail или проверка PIPESTATUS]
H -- Нет --> J[Нормальное продолжение]
I --> J --> K[Конец]Совместимость и особенности
- PIPESTATUS — Bash-специфичная функциональность. В POSIX /bin/sh её нет, значит скрипты, рассчитанные на /bin/sh, не должны полагаться на PIPESTATUS.
- set -o pipefail поддерживается в Bash и в некоторых других шеллах (ksh, zsh с флагами). Проверяйте целевой шелл.
Резюме
Важно применять набор опций и паттернов, которые соответствуют требованиям конкретного сценария: set -euo pipefail — хорошая точка старта для безопасных скриптов, но помните про контексты, где errexit не сработает автоматически. Добавляйте trap для централизованной обработки, используйте явные проверки для критичных шагов и включайте трассировку только для диагностики.
Важно: тестируйте сценарии отказа и документируйте ожидания по кодам возврата.
Критерии приёмки
- Скрипт корректно завершает работу с ненулевым кодом при ошибках важных команд.
- Ошибки внутри пайпов приводят к ненулевому коду при включённой опции pipefail.
- Обращения к неинициализированным переменным приводят к ошибке при включённой опции -u, если не задан дефолт.
Краткое содержание
- Используйте set -euo pipefail в большинстве скриптов.
- Для отладки применяйте set -x и trap ERR.
- В тестах и CI проверяйте поведение при отказах и некорректных входных данных.
Похожие материалы
Как устроить идеальную вечеринку для просмотра ТВ
Как распаковать несколько RAR‑файлов сразу
Приватный просмотр в Linux: как и зачем
Windows 11 не видит iPod — способы исправить
PS5: как настроить игровые пресеты