core code helper

Bash-скрипти, які не стріляють у ногу: set -euo pipefail і пастки

Шаблон надійного bash-скрипта, лапки навколо змінних, trap для прибирання, перевірка аргументів і shellcheck — усе, щоб скрипт не видалив зайве о третій ночі.

0 імпортували Імпортувати собі

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"' ERR

EXIT спрацює і при успіху, і при помилці, і при 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 Інші способи входу