Как обрабатывать пользовательские (кастомные) ошибки в Bash-скриптах?

Ответ

Обработка ошибок в Bash строится на кодах возврата (exit codes), функциях и команде trap. Я следую нескольким практикам для создания надежных скриптов.

1. Определение собственных кодов ошибок: Коды 1-127 доступны для пользовательских ошибок. Я документирую их в начале скрипта.

#!/bin/bash
# Коды ошибок:
# 1 - Общая ошибка
# 2 - Неверные аргументы
# 3 - Файл не найден
# 4 - Сетевая ошибка
# 5 - Ошибка проверки зависимостей

2. Функции с явным возвратом кода: Каждая функция возвращает 0 при успехе или свой код ошибки.

validate_port() {
    local port=$1
    # Проверка, что это число
    if ! [[ "$port" =~ ^[0-9]+$ ]]; then
        echo "Ошибка: порт должен быть числом" >&2
        return 2
    fi
    # Проверка диапазона
    if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
        echo "Ошибка: порт $port вне диапазона 1-65535" >&2
        return 3
    fi
    return 0
}

3. Обработка ошибок в основном потоке: Использую if или case для реакции на разные коды.

validate_port "$1"
exit_code=$?

case $exit_code in
    0) echo "Порт $1 валиден.";;
    2) echo "Ошибка валидации числа." >&2; exit 2;;
    3) echo "Ошибка диапазона порта." >&2; exit 3;;
    *) echo "Неизвестная ошибка." >&2; exit 1;;
esac

4. Глобальная обработка через trap: Для перехвата EXIT, ERR и сигналов (TERM, INT).

# Выход при любой ошибке (неудачная команда)
set -e
# Выход при использовании неинициализированных переменных
set -u

cleanup() {
    local exit_code=$?
    echo "Скрипт завершился с кодом $exit_code"
    # Удаление временных файлов, остановка процессов и т.д.
    rm -f "${TMP_FILE:-}"
}
# Вызов cleanup при любом выходе из скрипта
trap cleanup EXIT

# Перехват сигналов для graceful shutdown
trap 'echo "Получен сигнал завершения"; exit 143;' TERM INT

5. Логирование: Все сообщения об ошибках направляю в stderr (>&2), а стандартный вывод оставляю для полезных данных. Для сложных скриптов добавляю логирование в файл с указанием времени и уровня (ERROR, WARN).

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}

# Использование
log_error "Не удалось подключиться к хосту $DB_HOST"
exit 4

Ответ 18+ 🔞

А, ну ты глянь, обработка ошибок в баше! Тема, конечно, ебать мои старые костыли, но без неё — никуда. Сейчас разжую, как я это обычно делаю, чтобы скрипты не разваливались при первом же чихе.

1. Свои коды ошибок — это святое. Ну то есть, стандартные коды — это хорошо, но свои — лучше. Я их всегда в начале скрипта расписываю, чтобы потом самому не охуеть и другим понятно было.

#!/bin/bash
# Коды ошибок:
# 1 - Общая ошибка (ну, по дефолту)
# 2 - Аргументы кривые пришли
# 3 - Файл потерялся, блядь
# 4 - Сеть легла
# 5 - Зависимости не нашлись

2. Функции, которые не стесняются сказать, что нихуя не вышло. Каждая функция должна чётко отчитаться: 0 — всё заебись, не 0 — пошло что-то не так.

validate_port() {
    local port=$1
    # Проверяем, что это вообще цифры, а не хуйня какая-то
    if ! [[ "$port" =~ ^[0-9]+$ ]]; then
        echo "Ошибка: порт должен быть числом, а не '$port'" >&2
        return 2  # Код для "неверные аргументы"
    fi
    # А теперь смотрим, чтоб в нормальном диапазоне был
    if [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
        echo "Ошибка: порт $port — это ёперный театр! Должен быть от 1 до 65535" >&2
        return 3
    fi
    return 0  # Всё ок
}

3. В основном коде на эти коды надо реагировать. Нельзя просто так взять и проигнорировать. Тут либо if, либо case — кому как удобнее.

validate_port "$1"
exit_code=$?

case $exit_code in
    0) echo "Порт $1 — огонь, можно использовать.";;
    2) echo "Ну ты чё, цифры введи нормальные." >&2; exit 2;;
    3) echo "С портом ты явно переборщил, дружок." >&2; exit 3;;
    *) echo "Хуй знает, что случилось, но что-то пошло не так." >&2; exit 1;;
esac

4. Глобальные ловушки — наше всё. Вот это, блядь, мощнейшая штука — trap. Ставишь ловушку на выход, на ошибки, на сигналы — и скрипт убирает за собой, даже когда его убивают.

# Выходим сразу, если какая-то команда свалилась
set -e
# И если переменная не задана — тоже ата-та
set -u

cleanup() {
    local exit_code=$?
    echo "Всё, приехали. Код выхода: $exit_code"
    # Тут подчищаем весь свой бардак: временные файлы, процессы...
    rm -f "${TMP_FILE:-}"
}
# Эта функция вызовется ВСЕГДА, когда скрипт будет завершаться
trap cleanup EXIT

# А это чтобы на Ctrl+C или `kill` скрипт не сдох мгновенно, а вышел красиво
trap 'echo "Меня пытаются убить! Ладно, выхожу..."; exit 143;' TERM INT

5. Логирование — чтобы потом не было мучительно больно. Все ошибки — строго в stderr (>&2), чтобы не пачкать нормальный вывод. А для серьёзных проектов заводишь лог-файл с датами и уровнями.

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
}

# Используем
if ! ping -c 1 "$DB_HOST" &> /dev/null; then
    log_error "Хост $DB_HOST лежит, как тёща после застолья. Не дозвониться."
    exit 4
fi

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