0Pricing
DevOps Bootcamp · Урок

Обработчики trap для очистки и сигналов

Регистрируйте ловушки EXIT, ERR и INT, чтобы надёжно удалять временные файлы и откатывать незавершённые операции

«Обработчики trap для очистки и сигналов» — бесплатный урок DevOps Bootcamp на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DevOps Bootcamp, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DevOps Bootcamp содержит 4 уроков всего.

Зачем нужны обработчики ловушек

Когда сценарий Bash завершается — штатно, из-за ошибки или потому, что пользователь нажал Ctrl+C, — после него могут остаться временные файлы, частично записанные данные или нарушенное состояние. Без очистки эти остатки накапливаются и приводят к трудно диагностируемым проблемам.

Обработчики ловушек решают эту проблему: они регистрируют функцию или команду, которую Bash автоматически выполняет при получении определённого сигнала или псевдосигнала.

  • EXIT — срабатывает при завершении сценария по любой причине
  • ERR — срабатывает после того, как любая команда возвращает ненулевой статус завершения
  • INT — срабатывает, когда пользователь отправляет SIGINT (Ctrl+C)
  • TERM — срабатывает при SIGTERM (например, от kill)

Хорошо написанный сценарий регистрирует эти ловушки в самом начале, до начала любых рискованных операций.

Синтаксис встроенной команды trap

Встроенная команда trap регистрирует обработчик для одного или нескольких сигналов или псевдосигналов. Общий синтаксис:

trap 'command_or_function' SIGNAL [SIGNAL...]

Основные правила:

  • Первый аргумент — заключённая в кавычки строка кода оболочки (или имя функции), которая выполняется при срабатывании сигнала.
  • После обработчика можно указать несколько сигналов.
  • trap '' SIGNAL игнорирует этот сигнал (пустой обработчик).
  • trap - SIGNAL сбрасывает сигнал к поведению по умолчанию.

Ловушки наследуются функциями, вызываемыми в той же оболочке, но не наследуются вложенными оболочками, запущенными с помощью ( ) или &.

#!/usr/bin/env bash
# Syntax examples — not a full script

# Register a cleanup function on EXIT
trap cleanup EXIT

# Inline handler for INT and TERM
trap 'echo "Interrupted!"; exit 1' INT TERM

# Ignore SIGHUP
trap '' HUP

# Reset SIGPIPE to default
trap - PIPE

Ловушка EXIT — Ваша страховочная сеть

Псевдосигнал EXIT — самая важная ловушка для регистрации. Она срабатывает при завершении оболочки по любой причине: штатном завершении, вызове exit N, необработанной ошибке или сигнале. Поэтому она идеально подходит для удаления временных файлов.

Рекомендуемый подход:

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

Код завершения сценария сохраняется — код завершения обработчика ловушки игнорируется, если только внутри него явно не вызвать exit.

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

TMPFILE=$(mktemp /tmp/myapp.XXXXXX)

cleanup() {
    echo "Removing temp file: $TMPFILE" >&2
    rm -f "$TMPFILE"
}
trap cleanup EXIT

# Do real work
echo "Processing..." > "$TMPFILE"
sort "$TMPFILE" -o "$TMPFILE"
cp "$TMPFILE" /tmp/myapp_result.txt

echo "Done. Result saved."

Обработка нескольких временных файлов и каталогов

Реальные сценарии часто создают несколько временных объектов — файлы, каталоги, FIFO и файлы блокировок. Вместо того чтобы отслеживать каждый объект отдельно, соберите их в массив и удаляйте всё одной функцией очистки.

Этот подход легко масштабируется: при каждом создании нового временного ресурса добавляйте его в массив. Ловушка EXIT всегда обрабатывает весь список независимо от того, где завершается сценарий.

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

CLEANUP_TARGETS=()

add_cleanup() {
    CLEANUP_TARGETS+=("$1")
}

cleanup() {
    echo "Running cleanup..." >&2
    for target in "${CLEANUP_TARGETS[@]:-}"; do
        rm -rf "$target"
        echo "Removed: $target" >&2
    done
}
trap cleanup EXIT

# Create and track temp resources
TMPDIR_WORK=$(mktemp -d /tmp/work.XXXXXX)
add_cleanup "$TMPDIR_WORK"

TMPFILE_LOG=$(mktemp /tmp/run.XXXXXX.log)
add_cleanup "$TMPFILE_LOG"

echo "Working in $TMPDIR_WORK" | tee "$TMPFILE_LOG"
touch "$TMPDIR_WORK/output.txt"
echo "Step complete."

Ловушка ERR — перехват сбоев

Псевдосигнал ERR срабатывает после того, как простая команда возвращает ненулевой статус завершения, если активна опция оболочки set -e (errexit) или команда не находится внутри составной конструкции if, while либо ||/&&.

Используйте ловушку ERR, чтобы:

  • Записывать, какая строка завершилась с ошибкой, с помощью $LINENO и $BASH_COMMAND.
  • Запускать откат до срабатывания ловушки EXIT.
  • Выводить структурированные сообщения об ошибках в stderr.

Важно: ловушки ERR не срабатывают внутри функций, если явно не выполнить set -E (errtrace), чтобы функции унаследовали ловушку ERR от вызывающей оболочки.

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

on_error() {
    local exit_code=$?
    local line=$1
    echo "ERROR: command failed with exit code $exit_code at line $line" >&2
    echo "  Command: $BASH_COMMAND" >&2
}
trap 'on_error $LINENO' ERR

echo "Starting task..."
cp /nonexistent/file /tmp/   # This will fail
echo "This line is never reached."

Ловушка INT — корректная обработка Ctrl+C

Нажатие Ctrl+C отправляет SIGINT передней группе процессов. Без ловушки Bash немедленно завершает работу, и после него могут остаться незавершённые результаты.

Регистрация ловушки INT позволяет:

  • Вывести понятное пользователю сообщение об отмене.
  • Выполнить промежуточный откат перед передачей управления ловушке EXIT.
  • Завершить работу со значимым кодом статуса (130 — общепринятый код для SIGINT: 128 + 2).

После завершения обработчика INT вызовите exit 130, чтобы передать правильный код завершения. После этого ловушка EXIT сработает автоматически.

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

TMPFILE=$(mktemp /tmp/demo.XXXXXX)

cleanup() {
    rm -f "$TMPFILE"
    echo "Temp file removed." >&2
}

on_interrupt() {
    echo "" >&2
    echo "Caught SIGINT — cancelling gracefully." >&2
    exit 130   # EXIT trap (cleanup) runs automatically after this
}

trap cleanup EXIT
trap on_interrupt INT

echo "Running long task. Press Ctrl+C to cancel."
for i in $(seq 1 10); do
    echo "Step $i/10..."
    sleep 1
done
echo "All steps complete."

Объединение ловушек EXIT, ERR и INT

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

  • EXIT — срабатывает всегда и выполняет очистку файлов.
  • ERR — записывает команду, завершившуюся с ошибкой, и номер строки.
  • INT — выводит сообщение об отмене и завершает работу с кодом 130.

Поскольку EXIT всегда срабатывает последней, логика очистки должна находиться только в одном месте. Обработчики ERR и INT могут сосредоточиться на записи событий и изменении состояния, а затем передать фактическую очистку ловушке EXIT, вызвав exit.

Используйте set -eEuo pipefail в начале, чтобы ошибки в функциях также вызывали ERR, а pipefail перехватывал сбои внутри конвейеров.

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

TMPFILE=$(mktemp /tmp/combined.XXXXXX)
ROLLBACK_NEEDED=false

cleanup() {
    if [[ "$ROLLBACK_NEEDED" == true ]]; then
        echo "Rolling back partial changes..." >&2
    fi
    rm -f "$TMPFILE"
    echo "Cleanup done." >&2
}

on_error() {
    ROLLBACK_NEEDED=true
    echo "ERR at line $1: $BASH_COMMAND" >&2
}

on_interrupt() {
    echo "Interrupted by user." >&2
    exit 130
}

trap cleanup       EXIT
trap 'on_error $LINENO' ERR
trap on_interrupt  INT

echo "hello" > "$TMPFILE"
echo "Script finished successfully."

Динамический сброс и отключение ловушек

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

  • trap - SIGNAL сбрасывает сигнал к встроенному поведению по умолчанию.
  • trap '' SIGNAL полностью игнорирует сигнал (процесс нельзя завершить этим сигналом).
  • Новую обработку можно зарегистрировать в любой момент; действует последний вызов trap.

Распространённый подход — изменить ловушку EXIT после успешного выполнения рискованной операции, заменив обработчик отката простой очисткой.

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

TMPFILE=$(mktemp /tmp/staged.XXXXXX)

rollback() {
    echo "ROLLBACK: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

cleanup_only() {
    echo "CLEANUP: removing $TMPFILE" >&2
    rm -f "$TMPFILE"
}

# Start with rollback in case something fails during the risky phase
trap rollback EXIT

echo "Performing risky operation..."
echo "critical data" > "$TMPFILE"
# ... imagine more risky steps here ...

# Risky phase succeeded — switch to simple cleanup
trap cleanup_only EXIT

echo "Risky phase done. Now doing safe finalization."
cp "$TMPFILE" /tmp/staged_result.txt
echo "All done."

Передача кодов завершения через ловушки

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

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

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

TMPFILE=$(mktemp /tmp/exitcode.XXXXXX)

cleanup() {
    local original_exit=$?   # capture BEFORE any other command
    echo "Cleaning up (original exit: $original_exit)" >&2
    rm -f "$TMPFILE"
    # Exit with the original code so callers see the real result
    exit "$original_exit"
}
trap cleanup EXIT

echo "Doing work..."
echo "data" > "$TMPFILE"

# Simulate failure
if [[ ! -f /tmp/required_marker ]]; then
    echo "Required marker missing!" >&2
    exit 2
fi

echo "Done."

Ловушки в функциях и вложенных оболочках

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

  • Функции выполняются в той же оболочке и наследуют ловушки, но ERR распространяется на функции только при активной опции set -E (errtrace).
  • Вложенные оболочки (( ... )) начинают работу с копией ловушек родительской оболочки, но изменения внутри вложенной оболочки не влияют на родительскую.
  • Фоновые задания (cmd &) являются вложенными оболочками и не наследуют ловушки INT/TERM родительской оболочки (для них действует стандартная обработка сигналов).

Практическое правило: регистрируйте ловушки в основном теле сценария, используйте set -E, когда нужно распространить ERR на функции, и никогда не полагайтесь на наследование ловушек через &.

#!/usr/bin/env bash
set -eEuo pipefail   # -E = errtrace: ERR propagates into functions

on_error() {
    echo "ERR in function or main at line $1" >&2
}
trap 'on_error $LINENO' ERR

risky_function() {
    echo "Inside risky_function"
    ls /nonexistent_path   # triggers ERR — works because of set -E
}

# Subshell: has its own copy; changes don't affect parent
(
    trap '' ERR   # disable ERR only inside subshell
    ls /nonexistent_path 2>/dev/null || true
    echo "Subshell completed without triggering parent ERR"
)

risky_function

Практический шаблон: атомарная замена файла

Классический защищённый шаблон для Bash — атомарная замена файла: запись во временный файл с последующим переименованием на место исходного. Если до переименования произойдёт сбой, исходный файл останется неизменным. Ловушки делают этот подход надёжным.

Шаги:

  1. Создайте временный файл в той же файловой системе, что и файл назначения (чтобы mv выполняла атомарную операцию).
  2. Зарегистрируйте ловушку EXIT для удаления временного файла при досрочном завершении.
  3. Запишите и проверьте новое содержимое.
  4. Атомарно переименуйте файл (mv) — только теперь исходный файл будет заменён.
  5. Ловушка EXIT удалит временный файл, только если он всё ещё существует (после успешного mv он исчезает).
#!/usr/bin/env bash
set -euo pipefail

DEST="/tmp/important_config.conf"
TMPFILE=$(mktemp "$(dirname "$DEST")/.tmp.XXXXXX")

cleanup() {
    rm -f "$TMPFILE"   # no-op if mv already moved it
}
trap cleanup EXIT

# Write new content to temp file
cat > "$TMPFILE" <<'EOF'
[settings]
version=2
mode=production
EOF

# Validate before replacing
if ! grep -q 'version=' "$TMPFILE"; then
    echo "Validation failed — aborting replacement" >&2
    exit 1
fi

# Atomic rename — DEST is replaced only here
mv "$TMPFILE" "$DEST"
echo "Config updated atomically: $DEST"

Проверка знаний: наследование ловушки ERR

Проверьте, как ведёт себя ловушка ERR при работе с функциями в Bash.

Итоги: обработчики ловушек для очистки и сигналов

В этом уроке Вы узнали, как сделать сценарии Bash устойчивыми с помощью обработчиков ловушек:

  • EXIT — универсальная страховочная сеть: регистрируйте её сразу после создания любого временного ресурса и всегда сначала сохраняйте $?, чтобы сохранить исходный код завершения.
  • ERR позволяет записывать сведения о сбоях с помощью $LINENO и $BASH_COMMAND; используйте set -E, чтобы функции наследовали эту ловушку.
  • INT корректно обрабатывает Ctrl+C: выведите сообщение и завершите работу с кодом 130, чтобы EXIT выполнила очистку.
  • Собирайте временные ресурсы в массив и удаляйте их все одной функцией очистки.
  • Используйте trap - SIGNAL для сброса и trap '' SIGNAL для динамического игнорирования сигналов.
  • Шаблон атомарного переименования (mktemp → запись → проверка → mv) в сочетании с ловушкой EXIT гарантирует, что частичные записи никогда не повредят файл назначения.

Сочетание set -eEuo pipefail с правильно размещёнными ловушками создаёт надёжную основу для защищённого от сбоев рабочего сценария Bash.

Часто задаваемые вопросы

Урок «Обработчики trap для очистки и сигналов» бесплатный?

Да — полный текст урока «Обработчики trap для очистки и сигналов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DevOps Bootcamp, подпишись на CoddyKit PRO. Курс DevOps Bootcamp содержит 4 уроков всего.

Чему я научусь в уроке «Обработчики trap для очистки и сигналов»?

Регистрируйте ловушки EXIT, ERR и INT, чтобы надёжно удалять временные файлы и откатывать незавершённые операции Ты практикуешь DevOps Bootcamp с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DevOps Bootcamp?

Предыдущий опыт не требуется. DevOps Bootcamp на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Обработчики trap для очистки и сигналов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DevOps Bootcamp?

Да. Каждый урок DevOps Bootcamp включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Строгий режим с set -euo pipefail
  2. Обработчики trap для очистки и сигналов
  3. Безопасные временные файлы и каталоги блокировок
  4. Идемпотентные скрипты и логика повторных попыток с задержкой
← Назад к DevOps Bootcamp