Надёжные скрипты
set -euo pipefail: -e завершает при ошибке, -u — при необъявленной переменной, -o pipefail перехватывает сбои в конвейерах. trap убирает за собой при любом выходе. Функции с local предотвращают утечку переменных.
Bash-скрипт без set -euo pipefail — это скрипт, молча продолжающийся после сбоев. Команда, вернувшая код выхода 1, игнорируется. Опечатка в имени переменной раскрывается в пустую строку. Упавшая команда внутри конвейера маскируется успехом последней команды. Скрипт выводит «Готово!» и оставляет за собой наполовину выполненное, повреждённое или перезаписанное состояние. Три флага и один вызов trap полностью меняют картину: bash становится строгим интерпретатором с ранним выходом, который останавливается при первом признаке проблемы и запускает код очистки перед выходом. Это не опциональная полировка — это разница между скриптом, которому доверяют в проде, и скриптом, которому нянчатся.
После этого урока ты сможешь написать корректную строку shebang, применить set -euo pipefail и объяснить, что делает каждый флаг, зарегистрировать очистку через trap, писать функции с local для предотвращения утечки переменных, и знать три распространённые ловушки set -e.
#!/usr/bin/env bash — корректный shebang для переносимых bash-скриптов. Он просит ОС запустить тот bash, который первым встречается в $PATH, а не указывает жёстко /bin/bash (которого нет в некоторых системах — NixOS, ряд контейнеров).
#!/usr/bin/env bash
# Всегда первая строка — никаких пустых строк выше/bin/sh вызывает системную POSIX-оболочку — это может быть dash, ash или busybox, но не bash. Если скрипт использует [[ ]], массивы или другие bash-специфичные возможности, он обязан использовать #!/usr/bin/env bash. Запуск bash-скрипта под sh даёт загадочные ошибки разбора для конструкций, корректных в bash, но не в POSIX sh.
set -e завершает скрипт немедленно, если любая команда вернула ненулевой код выхода. Без него каждая упавшая команда молча игнорируется и выполнение продолжается.
set -e
cp /nonexistent /tmp/ # выходит здесь — cp упал
echo "Это никогда не выполнится" # пропущеноset -u трактует любую ссылку на незаданную переменную как ошибку и завершает скрипт.
set -u
echo "$UNDEFINED_VAR" # выходит: bash: UNDEFINED_VAR: unbound variableЭто перехватывает баг с опечаткой в переменной: $TEMP_DIR против $TMPDIR — одна задана, другая нет, и без -u незаданная молча раскрывается в пустую строку, вызывая rm -rf "" или cd "".
set -o pipefail заставляет конвейер упасть, если любая стадия упала, а не только последняя.
set -o pipefail
grep "шаблон" missing_file.txt | wc -l
# Без pipefail: завершается с 0 (wc успешен)
# С pipefail: завершается с ненулевым (grep упал)Ставь все три вместе в начале каждого скрипта:
set -euo pipefailtrap 'команда_очистки' СИГНАЛ регистрирует команду, которая выполнится при получении сигнала или выходе. Самый полезный сигнал — EXIT: он срабатывает при любом выходе, включая ошибки.
#!/usr/bin/env bash
set -euo pipefail
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT # всегда очищается, даже при ошибке
# ... тело скрипта использует $TMPDIR ...
cp important_data.txt "$TMPDIR/"
process "$TMPDIR/important_data.txt"Без trap сбой на середине оставляет $TMPDIR на диске. С ним bash запускает rm -rf "$TMPDIR" перед завершением процесса — независимо от того, как он завершается: нормально, через exit 1 или из-за необработанной ошибки.
Несколько сигналов: trap 'cleanup' EXIT INT TERM также обрабатывает Ctrl-C (INT) и kill (TERM).
Функции инкапсулируют переиспользуемую логику. local предотвращает утечку переменных в область вызывающего.
#!/usr/bin/env bash
set -euo pipefail
log() {
local level="$1"
local msg="$2"
echo "[$(date +%T)] [$level] $msg" >&2
}
backup_file() {
local src="$1"
local dest="${src}.bak.$(date +%s)"
cp "$src" "$dest"
log INFO "Скопировано $src в $dest"
}
backup_file /etc/nginx/nginx.confБез local переменные level, msg, src, dest — глобальные. Если вызывающая функция тоже использует переменную с именем dest, внутренняя функция перезапишет её. local ограничивает переменную стековым фреймом функции. Это не опциональный стиль — это предотвращение action-at-a-distance багов в скриптах длиннее пары функций.
Функции возвращают коды выхода как команды. return 0 — успех; return N — ошибка. Код выхода последней команды — это код выхода функции, если явно не вызывается return.
Три распространённые ловушки set -e, подстерегающие опытных инженеров.
Ловушка 1: команды в условиях if освобождены от действия -e. set -e не срабатывает, когда команда используется непосредственно как условие.
set -e
if grep -q "шаблон" file.txt; then echo "найдено"; fi # grep код 1 = не найдено, без abort
grep -q "шаблон" file.txt # ЭТО вызовет abort при коде 1Ловушка 2: последняя команда в конвейере определяет pipefail. С pipefail код выхода конвейера — это самый правый ненулевой код выхода. Если несколько стадий упали, сообщается только о последнем ненулевом.
Ловушка 3: local сама является командой с кодом выхода. Это тонкая ловушка:
set -e
get_value() {
local val=$(failing_command) # НЕВЕРНО: local возвращает 0, даже если $() упал
local val
val=$(failing_command) # ПРАВИЛЬНО: объявление отдельно от присваивания
}local val=$(cmd) вычисляет cmd, присваивает val, затем local выполняется и возвращает 0 независимо от результата — проглатывая ошибку. Сначала объяви local, затем присваивай отдельной строкой.
Скелет production-grade скрипта деплоя.
#!/usr/bin/env bash
set -euo pipefail
DEPLOY_DIR="/var/app"
RELEASE_DIR="${DEPLOY_DIR}/releases/$(date +%Y%m%d_%H%M%S)"
CURRENT_LINK="${DEPLOY_DIR}/current"
log() { echo "[$(date +%T)] $*" >&2; }
die() { log "ОШИБКА: $*"; exit 1; }
cleanup() {
local code=$?
if [[ $code -ne 0 ]]; then
log "Деплой провалился (код $code), откатываемся"
[[ -L "$CURRENT_LINK" ]] && log "Символическая ссылка current цела"
rm -rf "$RELEASE_DIR" 2>/dev/null || true
fi
}
trap cleanup EXIT
[[ $# -eq 1 ]] || die "Использование: $0 <архив>"
[[ -f "$1" ]] || die "Файл не найден: $1"
mkdir -p "$RELEASE_DIR"
tar -xzf "$1" -C "$RELEASE_DIR"
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
log "Деплой завершён: $RELEASE_DIR"Каждая переменная — local внутри функций. trap cleanup EXIT выполняется при любом сбое. set -euo pipefail гарантирует немедленный abort при ошибке tar или ln вместо создания наполовину задеплоенного релиза. die() централизует вывод ошибок.
▸Почему это работает
На macOS bash 3.2 set -e, set -u и set -o pipefail работают корректно. Специфика macOS: local val=$(cmd) — та же ловушка, то же решение, что и на Linux. Дополнительно: mktemp -d на macOS не требует аргумента с шаблоном (работает просто mktemp -d), тогда как GNU mktemp принимает и шаблон. Форма mktemp -d работает переносимо на обеих системах; mktemp -d /tmp/prefix.XXXXXX — только на Linux.
▸Частая ошибка
Опасный шаблон: set -e в комбинации с cd, за которым следует rm -rf. Если cd "$TARGET_DIR" провалится (неверный путь, права доступа) и set -e отсутствует, следующий rm -rf * выполнится в том каталоге, где скрипт находится сейчас — и, возможно, удалит не то дерево. С set -e скрипт завершается на упавшем cd. Форма с «поясом и подтяжками»: cd "$TARGET_DIR" || { echo "cd провалился" >&2; exit 1; } — явная даже при наличии set -e, потому что делает намерение очевидным для следующего читателя.
В скрипте с set -euo pipefail ты пишешь: local result=$(failing_command). Что происходит?
#!/usr/bin/env bash берёт корректный bash из $PATH вместо жёсткого указания /bin/bash. set -e завершает скрипт при ненулевом коде выхода; set -u — при обращении к незаданной переменной; set -o pipefail делает сбои в конвейере видимыми — используй все три, всегда. trap 'cleanup' EXIT гарантирует выполнение кода очистки независимо от способа завершения скрипта. local ограничивает переменные функции, предотвращая межфункциональное заражение. Ловушка local val=$(cmd): объявляй local и присваивай отдельно, чтобы set -e мог перехватить ошибку команды. Эти четыре практики превращают bash из хрупкой последовательности команд в надёжный, проверяемый инструмент автоматизации.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.