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

Обработка ошибок в 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
Автор
Редакция

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

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство