open atlas
↑ К треку
Командная строка CLI · 10 · 02

Скрипт обслуживания

set -euo pipefail, trap на EXIT, именованные функции и дисциплинированное экранирование — четыре привычки, вместе предотвращающие тихую потерю данных в bash-скриптах обслуживания.

CLI Senior ◷ 35 min
Уровень
ОсновыJuniorMiddleSenior

Ты годами писал однострочные команды. Теперь коллега просит автоматизировать ночное резервное копирование /var/app/data с ротацией — хранить последние семь дней, остальное удалять, вести лог. Начинаешь писать — и уже в трёх строках попадаешь в классическую ловушку: отсутствующий каталог тихо пропускает резервную копию, неудачный cp оставляет полунаписанные файлы, а rm -rf по неустановленной переменной удаляет корень продакшн-файловой системы. Каждая из этих историй — настоящая. Этот урок строит скрипт правильно с самого начала, используя четыре привычки, отличающие безопасную автоматизацию от замедленной бомбы.

Цель

После этого урока ты сможешь написать production-безопасный bash-скрипт обслуживания: set -euo pipefail для выхода при любой ошибке, trap cleanup EXIT для гарантированной очистки на любом пути выхода, именованные функции для организации, ротация резервных копий с временными метками через find -mtime, и корректное экранирование везде.

1

Открывай каждый скрипт с 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.

2

Регистрируй 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 уже зарегистрирован
3

Обрабатывай аргументы с валидацией. Скрипт, тихо игнорирующий отсутствующий аргумент или работающий с неверным путём, опасен. Проверяй $# (количество аргументов) в начале и выводи строку использования в 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". Неэкранированные пути с пробелами разбиваются на несколько аргументов — это самая распространённая причина сбоев «работает на моей машине».

4

Напиши функцию резервного копирования. Функция сохраняет логику читаемой и позволяет тестировать её независимо. Называй функции глаголами.

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}" без слеша копирует саму директорию, создавая лишний уровень вложенности.

5

Ротируй старые резервные копии с 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-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 6 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.