Гид по технологиям

Обработка ошибок в Bash: set -e, -u, pipefail и отладка

8 min read Bash Обновлено 25 Nov 2025
Ошибки в Bash: set -e, -u и pipefail
Ошибки в Bash: set -e, -u и pipefail

Быстрые ссылки

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

Ноутбук на синем фоне с терминалом Linux и приглашением командной строки.

В 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."
fi

set -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 и вывод трассировки только при падениях.

Практическое руководство по жёсткой защите скриптов (мини-методология)

  1. В начале каждого скрипта добавляйте:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
  1. Инициализируйте переменные явно.
  2. Используйте ${var:-default} там, где допускается дефолт.
  3. Оборачивайте операции с внешним вводом в проверки существования файлов и прав доступа.
  4. Добавьте trap для централизованного логирования ошибок и завершения.
  5. Ограничьте применение 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 там, где каждая команда критична.
    • Используйте явную проверку, если ожидается «контрабандная» обработка ошибок.
    • Для библиотечных функций предпочитайте возвращать коды и обрабатывать их явно.

Примеры типичных ошибок и как их избежать

  1. Ошибка: команда в if-условии приводит к аварии, когда этого не ждут. Решение: явно обработайте код или используйте || true в условии.

  2. Ошибка: PIPESTATUS перезаписывается следующей командой. Решение: записывайте PIPESTATUS сразу после пайпа в локальную переменную.

  3. Ошибка: 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 проверяйте поведение при отказах и некорректных входных данных.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Как устроить идеальную вечеринку для просмотра ТВ
Развлечения

Как устроить идеальную вечеринку для просмотра ТВ

Как распаковать несколько RAR‑файлов сразу
Инструменты

Как распаковать несколько RAR‑файлов сразу

Приватный просмотр в Linux: как и зачем
Приватность

Приватный просмотр в Linux: как и зачем

Windows 11 не видит iPod — способы исправить
Руководство

Windows 11 не видит iPod — способы исправить

PS5: как настроить игровые пресеты
Консоли

PS5: как настроить игровые пресеты

Как переключить камеру в Omegle на iPhone и Android
Руководство

Как переключить камеру в Omegle на iPhone и Android