Bash-скрипти, які не стріляють у ногу: set -euo pipefail і пастки
Шаблон надійного bash-скрипта, лапки навколо змінних, trap для прибирання, перевірка аргументів і shellcheck — усе, щоб скрипт не видалив зайве о третій ночі.
Bash за замовчуванням дуже поблажливий: команда впала — скрипт іде далі; змінна не задана — підставляється порожній рядок. У поєднанні з rm -rf це рецепт катастрофи:
rm -rf "$BUILD_DIR/"* # якщо BUILD_DIR порожня — це rm -rf /*Шаблон, з якого варто починати
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
die() { log "ПОМИЛКА: $*"; exit 1; }
usage() {
cat <<EOF
Використання: $(basename "$0") <environment> [--dry-run]
environment staging | production
EOF
}
main() {
[[ $# -ge 1 ]] || { usage; exit 2; }
local env="$1"
[[ "$env" =~ ^(staging|production)$ ]] || die "невідоме середовище: $env"
log "Деплой у $env"
}
main "$@"Що робить set -Eeuo pipefail
| Опція | Дія |
|---|---|
-e |
Зупинити скрипт, якщо команда повернула ненульовий код |
-u |
Помилка при використанні незаданої змінної |
-o pipefail |
Пайплайн падає, якщо впала будь-яка команда в ньому, а не лише остання |
-E |
trap ... ERR спрацьовує і всередині функцій |
Без pipefail команда curl -f https://api/... | jq . вважається успішною, навіть якщо curl отримав 500 — бо успішно відпрацював jq.
Увага
set -eмає неочевидні винятки: він не спрацьовує в умовахif, у лівій частині&&/||і в командах з!. Для критичних кроків перевіряйте результат явно:cmd || die "cmd не вдалося".
Лапки — завжди
file="мій звіт.pdf"
rm $file # ❌ видалить файли «мій» і «звіт.pdf»
rm "$file" # ✅
for f in $(ls *.log); do ... # ❌ ламається на пробілах
for f in ./*.log; do ... # ✅ glob безпечнішийПравило просте: кожна змінна в лапках, якщо ви свідомо не хочете розбиття на слова.
Захист від порожніх змінних
: "${BUILD_DIR:?BUILD_DIR не задано}" # впасти з повідомленням
rm -rf -- "${BUILD_DIR:?}/"* # ще один запобіжник прямо в команді
backup_dir="${BACKUP_DIR:-/var/backups}" # значення за замовчуваннямtrap: прибирання за будь-якого виходу
tmp="$(mktemp -d)"
cleanup() { rm -rf -- "$tmp"; }
trap cleanup EXIT
on_error() { log "Впало на рядку $1: $2"; }
trap 'on_error $LINENO "$BASH_COMMAND"' ERREXIT спрацює і при успіху, і при помилці, і при Ctrl+C — тимчасові файли не залишаться.
Ідемпотентність
Скрипт, який можна безпечно запустити вдруге, — найкращий скрипт:
mkdir -p /opt/app/releases # не впаде, якщо папка є
id deploy &>/dev/null || useradd -m deploy # створити, лише якщо немає
grep -qxF "$line" /etc/hosts || echo "$line" >> /etc/hostsРежим dry-run
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
run() {
if $DRY_RUN; then log "DRY-RUN: $*"; else "$@"; fi
}
run rsync -a --delete ./dist/ "deploy@$host:/opt/app/current/"shellcheck — обов'язково
Статичний аналізатор знаходить більшість описаних вище помилок автоматично:
shellcheck deploy.sh
# SC2086: Double quote to prevent globbing and word splitting.ПорадаДодайте shellcheck у CI і в редактор (є розширення для VS Code). А якщо скрипт виріс понад 150–200 рядків, з масивами, JSON і складною логікою, — подумайте про Python. Bash чудовий для склеювання команд, але не для програм.
Чому команда curl -f URL | jq . не зупиняє скрипт з set -e, коли сервер повертає 500?
Без pipefail код повернення пайплайна — це код останньої команди, тобто jq. Він успішно обробив порожній ввід, тож пайплайн вважається успішним. З set -o pipefail пайплайн поверне помилку curl, і скрипт зупиниться.
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу