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

Зачем обрабатывать ошибки
Обработка ошибок — неотъемлемая часть разработки скриптов. Даже идеальный код столкнётся с внешними изменениями: отсутствием каталогов, изменёнными правами, удалёнными файлами или изменениями окружения. По умолчанию 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Не придумывайте и не сохраняйте статусы произвольно — лучше документировать, какие коды возвращаются вашим скриптом и что они значат.

Принудительный выход с 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 с требуемым кодом.


Полезные советы и отладка
- Для подробной трассировки используйте
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) — пригодится при развитии и изменениях.
Практическая методика написания надёжного скрипта
Мини-методология (шаги):
- Определите критичные точки: операции, после которых нельзя продолжать при ошибке.
- В начале скрипта включите строгие опции: set -Eeuo pipefail (если подходит).
- Реализуйте единый обработчик ошибок с логированием и корректным exit-кодом.
- Для каждого внешнего вызова решите: попытаться восстановить, проигнорировать или завершиться.
- Добавьте unit- и интеграционные тесты, покрывающие отказные сценарии.
- Документируйте возвращаемые коды и поведение при ошибке.
Проверки и критерии приёмки
Критерии приёмки (минимум):
- Скрипт возвращает ненулевой код при ошибке.
- Ошибки логируются с информацией о коде и месте.
- Скрипт не продолжает выполнять критичные операции после неудачи.
- Наличие тестов, покрывающих основные отказные случаи.
Роль-по-ролевая чек-лист
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 и реакцию обработчика.
Безопасность и приватность
- Никогда не выводите секреты в лог без маскировки.
- Храните журналы в защищённом месте с ограниченным доступом.
- Анализируйте возможные ошибки, которые могут привести к утечке данных, и предотвращайте их (например, частичный вывод дампов в терминал).
Меры зрелости — как развивать практику
- Начальный: бытовые одноразовые скрипты без обработки ошибок.
- Упорядоченный: явная проверка ключевых команд, базовое логирование.
- Надёжный: set -Eeuo pipefail, trap, централизованный обработчик, тесты.
- Производственный: мониторинг, алерты, перезапуск через менеджер процессов, аудит логов.
Итог
Обработка ошибок в Bash — это комбинация инструментов и практик: проверка кодов выхода, использование || и && для управления потоком, set -Eeuo pipefail для строгого поведения, trap для аккуратной очистки и централизованный обработчик для логирования и унификации. Тестируйте отказные сценарии и документируйте поведение, чтобы автоматизация оставалась предсказуемой и безопасной.
Важно: планируйте, когда лучше попытаться восстановиться автоматически, а когда нужно немедленно остановиться — это ключ к надёжным скриптам.
Сводка:
- Проверяйте коды выхода и логируйте их.
- Используйте set -Eeuo pipefail, но учитывайте тонкости.
- Ловите сигналы и ERR через trap для аккуратного завершения.
- Пишите тесты на отказные сценарии и документируйте коды возврата.
Похожие материалы
Несколько аккаунтов Skype: Multi Skype Launcher
Журнал для работы: повысить продуктивность
Персональные звуки уведомлений на Android
Скачивание шоу Hulu для офлайн‑просмотра
Microsoft Start: персонализированная новостная лента