Скрипт обслуживания
set -euo pipefail, trap на EXIT, именованные функции и дисциплинированное экранирование — четыре привычки, вместе предотвращающие тихую потерю данных в bash-скриптах обслуживания.
Ты годами писал однострочные команды. Теперь коллега просит автоматизировать ночное резервное копирование /var/app/data с ротацией — хранить последние семь дней, остальное удалять, вести лог. Начинаешь писать — и уже в трёх строках попадаешь в классическую ловушку: отсутствующий каталог тихо пропускает резервную копию, неудачный cp оставляет полунаписанные файлы, а rm -rf по неустановленной переменной удаляет корень продакшн-файловой системы. Каждая из этих историй — настоящая. Этот урок строит скрипт правильно с самого начала, используя четыре привычки, отличающие безопасную автоматизацию от замедленной бомбы.
После этого урока ты сможешь написать production-безопасный bash-скрипт обслуживания: set -euo pipefail для выхода при любой ошибке, trap cleanup EXIT для гарантированной очистки на любом пути выхода, именованные функции для организации, ротация резервных копий с временными метками через find -mtime, и корректное экранирование везде.
Открывай каждый скрипт с set -euo pipefail. Эта одна строка переключает bash из разрешительного в строгий режим:
-e— немедленно завершает работу, если любая команда возвращает ненулевой статус (кроме случаев, когда она частьif,||,&&или условияwhile).-u— трактует неустановленные переменные как ошибки, а не разворачивает их в пустую строку.rm -rf "$MISSING_VAR/data"превращается вrm -rf "/data"без-u; с ним bash завершает работу до выполнения команды.-o pipefail— без этого флага конвейер вродеbroken_command | tee output.logвозвращает0, еслиteeуспешен, скрывая сбой. Сpipefailкод выхода конвейера — это крайний ненулевой статус.
#!/usr/bin/env bash
set -euo pipefailСтавь это второй строкой каждого скрипта, работающего без надзора. Шебанг #!/usr/bin/env bash предпочтительнее #!/bin/bash, поскольку находит bash из PATH, что важно на системах, где bash находится не в /bin.
Регистрируй trap на EXIT для гарантированной очистки. trap КОМАНДА СИГНАЛ запускает КОМАНДУ при получении СИГНАЛА. EXIT — особый сигнал: он срабатывает при любом выходе: нормальное завершение, exit N, выход по ошибке из -e и даже SIGTERM. Это делает trap cleanup EXIT правильным паттерном для логики очистки.
TMPDIR_WORK=""
cleanup() {
if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
rm -rf "$TMPDIR_WORK"
echo "[cleanup] удалён $TMPDIR_WORK" >&2
fi
}
trap cleanup EXITПорядок важен: регистрируй trap до создания ресурса. Если скрипт завершится между mktemp и trap, очистка никогда не выполнится. Сначала trap, потом выделение.
TMPDIR_WORK=$(mktemp -d) # безопасно: trap уже зарегистрированОбрабатывай аргументы с валидацией. Скрипт, тихо игнорирующий отсутствующий аргумент или работающий с неверным путём, опасен. Проверяй $# (количество аргументов) в начале и выводи строку использования в stderr.
usage() {
echo "Использование: $0 <src_dir> <backup_dir> [keep_days]" >&2
exit 1
}
[[ $# -lt 2 ]] && usage
SRC="${1:?требуется исходный каталог}"
DEST="${2:?требуется каталог резервных копий}"
KEEP="${3:-7}" # по умолчанию: хранить 7 дней резервных копий${VAR:?сообщение} — расширение параметра, которое завершает работу с сообщением об ошибке, если VAR пуст или не установлен — более строгая проверка, чем ручная защита [[ -z ]]. ${VAR:-default} задаёт запасное значение без завершения работы.
Всегда экранируй каждую переменную, содержащую путь: "$SRC", "$DEST". Неэкранированные пути с пробелами разбиваются на несколько аргументов — это самая распространённая причина сбоев «работает на моей машине».
Напиши функцию резервного копирования. Функция сохраняет логику читаемой и позволяет тестировать её независимо. Называй функции глаголами.
do_backup() {
local src="$1"
local dest="$2"
local ts
ts=$(date +%Y%m%d_%H%M%S)
local target="${dest}/backup_${ts}"
echo "[backup] копирование ${src} → ${target}" >&2
cp -a "${src}/." "${target}"
echo "[backup] готово: ${target}" >&2
}local предотвращает утечку переменных в глобальную область. cp -a (режим архива) сохраняет права доступа, временные метки и симлинки — правильный выбор для резервной копии. cp -a "${src}/." "${target}" копирует содержимое src в target (трюк с завершающим /.); использование "${src}" без слеша копирует саму директорию, создавая лишний уровень вложенности.
Ротируй старые резервные копии с find -mtime. -mtime +N совпадает с файлами, изменёнными более чем N×24 часов назад. -maxdepth 1 предотвращает рекурсию в поддиректории, что критично, когда dest содержит только что созданную резервную копию.
rotate_backups() {
local dest="$1"
local keep="$2"
echo "[rotate] удаление резервных копий старше ${keep} дней в ${dest}" >&2
find "${dest}" -maxdepth 1 -name 'backup_*' -type d -mtime "+${keep}" \
-exec rm -rf {} +
}-exec rm -rf {} + группирует все совпадения в один вызов rm (эффективнее, чем -exec ... \;, который порождает процесс для каждого совпадения). Шаблон backup_* защищает от случайного удаления несвязанных директорий в dest.
Полный скрипт: ночное резервное копирование с ротацией.
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Использование: $0 <src_dir> <backup_dir> [keep_days]" >&2
exit 1
}
[[ $# -lt 2 ]] && usage
SRC="${1:?требуется исходный каталог}"
DEST="${2:?требуется каталог резервных копий}"
KEEP="${3:-7}"
TMPDIR_WORK=""
cleanup() {
if [[ -n "$TMPDIR_WORK" && -d "$TMPDIR_WORK" ]]; then
rm -rf "$TMPDIR_WORK"
echo "[cleanup] удалён $TMPDIR_WORK" >&2
fi
}
trap cleanup EXIT
do_backup() {
local src="$1"
local dest="$2"
local ts
ts=$(date +%Y%m%d_%H%M%S)
local target="${dest}/backup_${ts}"
mkdir -p "${dest}"
echo "[backup] ${src} → ${target}" >&2
cp -a "${src}/." "${target}"
echo "[backup] готово" >&2
}
rotate_backups() {
local dest="$1"
local keep="$2"
echo "[rotate] удаление резервных копий старше ${keep} дней" >&2
find "${dest}" -maxdepth 1 -name 'backup_*' -type d -mtime "+${keep}" \
-exec rm -rf {} +
echo "[rotate] готово" >&2
}
do_backup "$SRC" "$DEST"
rotate_backups "$DEST" "$KEEP"
echo "[main] резервное копирование завершено" >&2Запуск: ./backup.sh /var/app/data /mnt/backups 7
Trap срабатывает в конце в любом случае — в этом минимальном примере TMPDIR_WORK пуст, поэтому очистка — это no-op. Расширяй скрипт, создавая временный промежуточный каталог через TMPDIR_WORK=$(mktemp -d) перед резервным копированием, делай работу внутри, затем атомарно перемещай через mv — trap очистит промежуточный каталог, если что-то пойдёт не так в процессе копирования.
▸Почему это работает
На macOS / BSD find в некоторых граничных случаях интерпретирует -mtime немного иначе, а cp -a недоступен — используй вместо него cp -pR для архивоподобного копирования. Формат date +%Y%m%d_%H%M%S работает идентично в GNU и BSD date. При ротации через find -mtime проверяй с -print перед -exec rm -rf — всегда убеждайся, что будет удалено.
▸Частая ошибка
Распространённая ошибка — регистрировать trap после ресурса, который он защищает. Если скрипт завершится между TMPDIR_WORK=$(mktemp -d) и trap cleanup EXIT, временная директория останется. Регистрируй trap сначала, инициализируй TMPDIR_WORK="", затем присваивай — так проверка [[ -n "$TMPDIR_WORK" ]] в cleanup безопасно обрабатывает пустой случай. Вторая распространённая ошибка: забытые кавычки вокруг "${dest}" в команде find. Пробел в пути разбивает его на два аргумента, find завершается с ошибкой, оставляя старые резервные копии нетронутыми.
Скрипт использует set -euo pipefail. Строка rm -rf $WORKDIR/tmp запускается, а WORKDIR не установлен. Что произойдёт?
Надёжный скрипт обслуживания строится на четырёх привычках: set -euo pipefail немедленно завершается при ошибках, неустановленных переменных и скрытых сбоях в конвейерах; trap cleanup EXIT гарантирует выполнение очистки на любом пути выхода; именованные функции с переменными local сохраняют логику читаемой и изолированной; последовательное экранирование всех переменных-путей предотвращает баги от разделения слов. Регистрируй trap до выделения ресурсов. Используй ${VAR:?сообщение} для валидации обязательных аргументов и ${VAR:-default} для необязательных. Для ротации find -maxdepth 1 -mtime +N -exec rm -rf {} + — безопасный и эффективный вариант. Эти привычки компонуются: каждая устраняет класс сбоев, невидимый без неё.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.