Как обрабатываете ошибки в bash-скриптах

Ответ

В скриптах для автоматизации инфраструктуры надежная обработка ошибок критична. Я использую комбинацию строгих режимов, перехвата сигналов и явных проверок.

Базовые директивы для строгого режима: Добавляю в начало скрипта:

#!/usr/bin/env bash
set -euo pipefail
  • set -e (errexit): Немедленный выход при ошибке в любой команде (ненулевой код возврата).
  • set -u (nounset): Выход при попытке использования неинициализированной переменной.
  • set -o pipefail: Код возврата пайплайна считается неудачным, если ошиблась хотя бы одна команда в цепочке, а не только последняя.

Перехват сигналов и очистка (trap): Использую trap для гарантированного выполнения cleanup-действий (удаление временных файлов, откат изменений) даже при аварийном завершении.

#!/usr/bin/env bash
set -euo pipefail

TEMP_FILE="/tmp/myscript.$$"

cleanup() {
    exit_code=$?
    echo "Выполняется cleanup..." >&2
    rm -f "$TEMP_FILE"
    # Дополнительные действия: удаление временного namespace в k8s, отключение от VPN и т.д.
    exit $exit_code
}
# Срабатывает при EXIT, ошибке (ERR), прерывании (INT) и завершении (TERM)
trap cleanup EXIT ERR INT TERM

# Основная логика скрипта
if ! command -v jq > /dev/null; then
    echo "ОШИБКА: jq не установлен. Установите пакет jq." >&2
    exit 1
fi

config=$(cat config.json | jq '.settings') || {
    echo "Не удалось распарсить config.json" >&2
    exit 1
}

Практические примеры из DevOps:

  • Проверка доступности сервиса:
    if ! curl -sf --max-time 5 http://${SERVICE_URL}/health > /dev/null; then
        echo "Сервис ${SERVICE_URL} недоступен" >&2
        # Можно отправить алерт в Slack/PagerDuty
        exit 1
    fi
  • Работа с Terraform/AWS CLI:
    aws ec2 describe-instances --instance-ids "$INSTANCE_ID" > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
        echo "Инстанс ${INSTANCE_ID} не найден" >&2
        exit 1
    fi

    Логирование всех действий и ошибок в структурированном виде (например, JSON) упрощает дальнейший анализ в системах вроде ELK или Loki.

Ответ 18+ 🔞

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

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

#!/usr/bin/env bash
set -euo pipefail

Объясняю, что это за ёперный театр:

  • set -e — это чтобы скрипт сразу сдыхал, если любая команда внутри него завершилась с ошибкой. Не будет он дальше пытаться работать с битым состоянием, ебать копать.
  • set -u — это моя любимая. Если скрипт попытается использовать переменную, которой нет — сразу остановка. Больше никаких пустых значений, которые потом где-то в глубине всё ебут. Удивление пиздец, когда начинаешь это использовать и понимаешь, сколько говнокода писал раньше.
  • set -o pipefail — вот это вообще хитрая жопа. Без этого, если в цепочке команд (|) падает первая, а вторая норм, то весь пайплайн считается успешным. Это же пиздец! С этой опцией пайплайн провалится, если хоть где-то в цепочке ошибка.

Дальше — ловушка, или trap. Это на случай, если скрипт убьют сигналом или он сам сдохнет. Надо же прибраться за собой, удалить временные файлы, откатить какие-то изменения, а то останется после тебя срака неубранная.

#!/usr/bin/env bash
set -euo pipefail

TEMP_FILE="/tmp/myscript.$$"

cleanup() {
    exit_code=$?
    echo "Выполняется cleanup..." >&2
    rm -f "$TEMP_FILE"
    # Дополнительные действия: удаление временного namespace в k8s, отключение от VPN и т.д.
    exit $exit_code
}
# Срабатывает при EXIT, ошибке (ERR), прерывании (INT) и завершении (TERM)
trap cleanup EXIT ERR INT TERM

# Основная логика скрипта
if ! command -v jq > /dev/null; then
    echo "ОШИБКА: jq не установлен. Установите пакет jq." >&2
    exit 1
fi

config=$(cat config.json | jq '.settings') || {
    echo "Не удалось распарсить config.json" >&2
    exit 1
}

Видишь? Явные проверки. Прежде чем использовать jq, я проверяю, есть ли он вообще. И если команда с jq свалится, у меня есть блок || с явным выходом и сообщением. Не просто молча умрёт где-то внутри.

А вот практические примеры, чтобы совсем понятно было, о чём речь:

  • Проверка сервиса. Ты же не хочешь деплоить на сдохший кластер?

    if ! curl -sf --max-time 5 http://${SERVICE_URL}/health > /dev/null; then
        echo "Сервис ${SERVICE_URL} недоступен" >&2
        # Можно отправить алерт в Slack/PagerDuty
        exit 1
    fi

    Если curl не смог — всё, стоп. И пишешь в stderr (>&2), а не в stdout, чтобы логи правильно парсить.

  • Работа с облаком. Запрос к AWS CLI, который может провалиться.

    aws ec2 describe-instances --instance-ids "$INSTANCE_ID" > /dev/null 2>&1
    if [[ $? -ne 0 ]]; then
        echo "Инстанс ${INSTANCE_ID} не найден" >&2
        exit 1
    fi

    Проверяешь код возврата. Не нашёл инстанс? Ну и хуй с ним, дальше не работаем, выходим с ошибкой.

И главный совет в конце: логируй всё, что происходит, и особенно ошибки, в нормальном, структурированном виде. Хоть в JSON. Потом, когда этот скрипт упадёт ночью и разбудит тебя алертом, ты в Loki или ELK быстро найдешь, на каком именно шаге и почему всё пошло по пизде. А не будешь, как дурак, гадать. Э, бошка, думай!