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

Обработка ошибок в Bash-скриптах на Linux

• 7 min read • Linux • Обновлено 10 Dec 2025
Обработка ошибок в Bash-скриптах
Обработка ошибок в Bash-скриптах

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

  • Обработка ошибок в скриптах
  • Проверка кода выхода
  • Принудительный выход через set
  • Использование trap для ошибок
  • Полезные советы и отладка

Ноутбук с терминалом Bash на Linux, показывающий командную строку

Зачем обрабатывать ошибки

Обработка ошибок — неотъемлемая часть разработки скриптов. Даже идеальный код столкнётся с внешними изменениями: отсутствием каталогов, изменёнными правами, удалёнными файлами или изменениями окружения. По умолчанию Bash лишь выводит сообщение об ошибке и продолжает выполнение, что может привести к неожиданным последствиям, если последующие операции зависят от предыдущих.

Хорошая стратегия обработки ошибок позволяет:

  • предсказать поведение при неисправностях,
  • аккуратно завершать работу и освобождать ресурсы,
  • уведомлять оператора или систему мониторинга,
  • при необходимости попытаться восстановить состояние.

Важно разграничивать два сценария: пытаться автоматически исправить ошибку (например, создать отсутствующий каталог) или немедленно завершить выполнение (fail fast), чтобы избежать повреждения данных.

Проверка кода выхода

Каждая команда в Unix/Linux возвращает код выхода: 0 означает успех, любое ненулевое значение — ошибку. В Bash последний код выхода доступен в переменной $?. Также полезна переменная ${LINENO} для указания строки, где обнаружилась ошибка.

Ниже — минимальный пример с явной проверкой и корректным синтаксисом if:

#!/bin/bash

if ! bad_command; then
  echo "bad_command flagged an error."
  exit 1
fi

Сделайте сценарий исполняемым:

chmod +x bad_command.sh

Если bad_command не существует, код выхода будет ненулевым и выполнится блок then. Команда exit 1 возвращает код выхода вызывающему процессу — это важно, если ваш скрипт вызывают другие скрипты или механизмы автоматизации.

Иногда удобнее применять логический OR (||) для вызова обработчика ошибок справа от команды:

command_1 || command_2

Если command_1 завершится с кодом 0, command_2 не выполнится. Если command_1 вернёт ошибку, выполнится command_2.

Пример с централизованным обработчиком ошибок:

#!/bin/bash

error_handler() {
  echo "Error: ($?) $1"
  exit 1
}

bad_command || error_handler "bad_command failed, Line: ${LINENO}"

Функция error_handler получает код ошибки в $? и сообщение в $1, затем завершает выполнение со статусом 1. Такой подход упрощает единообразную реакцию на ошибки в разных частях скрипта.

Разбор кода выхода

Иногда имеет смысл различать конкретные коды выхода и действовать по-разному. Простейшая схема:

exit_code=$?

if [ $exit_code -eq 1 ]; then
  echo "Operation not permitted"
elif [ $exit_code -eq 2 ]; then
  echo "Misuse of shell builtins"
elif [ $exit_code -eq 127 ]; then
  echo "Command not found"
elif [ $exit_code -ge 128 ]; then
  echo "Fatal error or terminated by signal"
fi

Не придумывайте и не сохраняйте статусы произвольно — лучше документировать, какие коды возвращаются вашим скриптом и что они значат.

Отладочная диаграмма вызова обработчика ошибок через логический OR

Принудительный выход с set

Если вы хотите, чтобы скрипт немедленно завершался при любой ошибке, используйте опции set. Они позволяют реализовать поведение “fail fast”:

  • -e (errexit): выйти при ненулевом коде выхода команды;
  • -E: распространяет обработку ошибок на функции и под-оболочки;
  • -u: считать обращение к неинициализированной переменной ошибкой;
  • -o pipefail: при конвейере команд возвращать код ошибки первой неуспешной команды, а не последней.

Рекомендуемая строка в начале скрипта:

set -Eeuo pipefail

Пример проблемы с неинициализированной переменной и корректным завершением:

#!/bin/bash

set -Eeuo pipefail

echo "$unset_variable"

echo "Do we see this line?"

При запуске ./unset-var.sh скрипт завершится на первой echo, и строка “Do we see this line?” не будет напечатана.

Важно: set -e имеет тонкости в сочетании с некоторыми конструкциями (например, в boolean-условиях, списках команд с &&/||, командами в if/while и т.д.). Поэтому тестируйте сценарии, где вы рассчитываете на автоматический выход.

trap для сигналов и ошибок

Команда trap позволяет регистрировать обработчики на сигналы и события. Это удобно для аккуратного завершения: очистки временных файлов, вывода диагностической информации и возвращения корректного кода ошибки.

Пример перехвата Ctrl+C (SIGINT):

#!/bin/bash

trap "echo -e '\nTerminated by Ctrl+C'; exit" SIGINT

counter=0
while true; do
  echo "Loop number:" $((++counter))
  sleep 1
done

При нажатии Ctrl+C вы увидите сообщение и скрипт завершится через trap.

Также можно ловить внутреннее событие ERR, которое срабатывает при ненулевом коде команды (в сочетании с set -E наложится и на функции):

#!/bin/bash

trap 'error_handler $? $LINENO' ERR

error_handler() {
  echo "Error: ($1) occurred on $2"
  # возможно: логирование в файл, уведомление оператора, очистка
}

main() {
  echo "Inside main() function"
  bad_command
  second
  third
  exit $?
}

second() {
  echo "After call to main()"
  echo "Inside second() function"
}

third() {
  echo "Inside third() function"
}

main

В этом примере при ошибке в bad_command будет вызвана функция error_handler с кодом ошибки и номером строки. Если нужно немедленно завершить скрипт внутри обработчика, добавьте exit с требуемым кодом.

Перехват сигнала SIGINT и аккуратное завершение скрипта

Использование trap с ERR для перехвата ошибок в скрипте

Полезные советы и отладка

  • Для подробной трассировки используйте bash -x script.sh. Bash покажет команды после подстановки аргументов и прежде чем их выполнить. Это упрощает поиск причин неправильного поведения.
  • Логируйте события: запись в лог-файл с метками времени и уровнями важности помогает восстановить цепочку действий.
  • В тестовой среде моделируйте ошибки: удаляйте файлы, меняйте права, искусственно инициируйте сбои, чтобы проверить реакцию скрипта.
  • Избегайте использования set -e в библиотеках или фрагментах кода, которые используются в разных контекстах, где ожидаются частичные ошибки.
  • Проверяйте возвратные значения внешних команд (rsync, cp, docker и т.д.), даже если они обычно «надёжны».

Когда простая обработка ошибок не подходит

Контр-примеры и ограничения подходов:

  • set -e может скрыть ошибку в условии if или в сложной конструкции, если не учитывать особенности оболочки. В таких местах лучше явно проверять код выхода.
  • trap ERR не всегда даёт корректный контекст в сложных конвейерах без set -o pipefail.
  • В многопоточном или параллельном окружении (GNU parallel, background jobs) управление ошибками сложнее — требуется централизованная очередь ошибок или отдельный монитор процессов.

Альтернативные подходы

  • Запуск критичных операций под контролем менеджера выполнения (systemd, cron с проверками), чтобы при падении автоматически перезапустить задачу.
  • Использование языков с исключениями (Python, Go) для сложной логики и затем вызов внешних утилит через обёртки.
  • Тестирование скриптов через unit-тесты для Bash (например, shunit2 или bats) — пригодится при развитии и изменениях.

Практическая методика написания надёжного скрипта

Мини-методология (шаги):

  1. Определите критичные точки: операции, после которых нельзя продолжать при ошибке.
  2. В начале скрипта включите строгие опции: set -Eeuo pipefail (если подходит).
  3. Реализуйте единый обработчик ошибок с логированием и корректным exit-кодом.
  4. Для каждого внешнего вызова решите: попытаться восстановить, проигнорировать или завершиться.
  5. Добавьте unit- и интеграционные тесты, покрывающие отказные сценарии.
  6. Документируйте возвращаемые коды и поведение при ошибке.

Проверки и критерии приёмки

Критерии приёмки (минимум):

  • Скрипт возвращает ненулевой код при ошибке.
  • Ошибки логируются с информацией о коде и месте.
  • Скрипт не продолжает выполнять критичные операции после неудачи.
  • Наличие тестов, покрывающих основные отказные случаи.

Роль-по-ролевая чек-лист

SRE / DevOps:

  • Убедиться, что скрипты возвращают осмысленные коды ошибок.
  • Добавить мониторинг и алерты на ошибки в автоматизации.

Разработчик:

  • Добавить обработчики ошибок и unit-тесты.
  • Документировать поведение и коды возврата.

Тестировщик:

  • Смоделировать отказ внешних зависимостей.
  • Проверить, что скрипт корректно завершает работу и логирует причину.

Шаблоны и сниппеты (cheat sheet)

Единый обработчик ошибок + лог в файл:

#!/bin/bash
set -Eeuo pipefail
LOGFILE="/var/log/myscript.log"

log() { echo "$(date -u +'%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOGFILE"; }

error_handler() {
  local exit_code=${1:-$?}
  local lineno=${2:-${LINENO}}
  log "ERROR: code=$exit_code line=$lineno message=$3"
  exit "$exit_code"
}

trap 'error_handler $? ${LINENO} "Unexpected error"' ERR

# Пример использования
mkdir -p /some/dir || error_handler $? ${LINENO} "Failed to create /some/dir"

Сценарий проверки внешней команды с попыткой восстановления:

command || {
  log "Command failed, retrying"
  sleep 1
  command || error_handler $? ${LINENO} "command failed after retry"
}

Decision flow — как решать стратегию при ошибке

flowchart TD
  A[Произошла ошибка] --> B{Критична ли ошибка?}
  B -- Да --> C[Завершить скрипт с логом и кодом ошибки]
  B -- Нет --> D{Можно ли восстановить автоматически?}
  D -- Да --> E[Попытаться восстановить]
  E --> F{Успех?}
  F -- Да --> G[Продолжить выполнение]
  F -- Нет --> C
  D -- Нет --> H[Пропустить и логировать]
  H --> G

Тестовые случаи и приёмка

Пример тест-кейсов:

  • Удалить зависимый файл и запустить скрипт: проверить, что он логирует и создаёт файл/завершает работу по сценарию.
  • Дать скрипту ограниченные права: проверить обработку ошибок доступа.
  • Смоделировать несуществующую внешнюю команду: проверить код 127 и реакцию обработчика.

Безопасность и приватность

  • Никогда не выводите секреты в лог без маскировки.
  • Храните журналы в защищённом месте с ограниченным доступом.
  • Анализируйте возможные ошибки, которые могут привести к утечке данных, и предотвращайте их (например, частичный вывод дампов в терминал).

Меры зрелости — как развивать практику

  1. Начальный: бытовые одноразовые скрипты без обработки ошибок.
  2. Упорядоченный: явная проверка ключевых команд, базовое логирование.
  3. Надёжный: set -Eeuo pipefail, trap, централизованный обработчик, тесты.
  4. Производственный: мониторинг, алерты, перезапуск через менеджер процессов, аудит логов.

Итог

Обработка ошибок в Bash — это комбинация инструментов и практик: проверка кодов выхода, использование || и && для управления потоком, set -Eeuo pipefail для строгого поведения, trap для аккуратной очистки и централизованный обработчик для логирования и унификации. Тестируйте отказные сценарии и документируйте поведение, чтобы автоматизация оставалась предсказуемой и безопасной.

Важно: планируйте, когда лучше попытаться восстановиться автоматически, а когда нужно немедленно остановиться — это ключ к надёжным скриптам.

Сводка:

  • Проверяйте коды выхода и логируйте их.
  • Используйте set -Eeuo pipefail, но учитывайте тонкости.
  • Ловите сигналы и ERR через trap для аккуратного завершения.
  • Пишите тесты на отказные сценарии и документируйте коды возврата.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

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

Несколько аккаунтов Skype: Multi Skype Launcher
Программное обеспечение

Несколько аккаунтов Skype: Multi Skype Launcher

Журнал для работы: повысить продуктивность
Productivity

Журнал для работы: повысить продуктивность

Персональные звуки уведомлений на Android
Android.

Персональные звуки уведомлений на Android

Скачивание шоу Hulu для офлайн‑просмотра
Стриминг

Скачивание шоу Hulu для офлайн‑просмотра

Microsoft Start: персонализированная новостная лента
Новости

Microsoft Start: персонализированная новостная лента

Как изменить имя в Epic Games быстро
Гайды

Как изменить имя в Epic Games быстро