NPV-AM

← Все статьи

Bash: паттерны и антипаттерны скриптов

6 мар 2026 · 10 мин чтения

Строгий режим

Каждый серьезный скрипт должен начинаться с set -euo pipefail. Это предотвращает тихие ошибки и неожиданное поведение:

#!/usr/bin/env bash
set -euo pipefail

# -e: завершить скрипт при ошибке
# -u: ошибка при использовании неинициализированной переменной
# -o pipefail: ошибка в любом элементе pipe приводит к ошибке

Без set -e команда rm -rf "$DIR"/* удалит текущую директорию, если переменная DIR пуста, и скрипт продолжит работу.

Обработка аргументов

#!/usr/bin/env bash
set -euo pipefail

usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS] FILE

Options:
    -h, --help      Show this help
    -v, --verbose   Enable verbose output
    -o OUTPUT       Output file (default: stdout)
EOF
    exit "${1:-0}"
}

VERBOSE=false
OUTPUT=""

while [[ $# -gt 0 ]]; do
    case "$1" in
        -h|--help) usage 0 ;;
        -v|--verbose) VERBOSE=true; shift ;;
        -o) OUTPUT="$2"; shift 2 ;;
        -*) echo "Unknown option: $1" >&2; usage 1 ;;
        *) FILE="$1"; shift ;;
    esac
done

[[ -z "${FILE:-}" ]] && { echo "FILE is required" >&2; usage 1; }

Антипаттерн: парсинг аргументов через $1

# ПЛОХО: хрупко, не обрабатывает кавычки
if [ "$1" = "--verbose" ]; then
    VERBOSE=1
fi
if [ "$2" = "" ]; then
    echo "Need argument"
    exit 1
fi
Всегда используйте [[ ]] вместо [ ] — он безопаснее и поддерживает регулярные выражения.

Работа с файлами

# Безопасное создание временного файла
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT

# Или временная директория
TMPCDIR=$(mktemp -d)
trap 'rm -rf "$TMPCDIR"' EXIT

# Безопасное чтение файла
while IFS= read -r line; do
    echo "Processing: $line"
done < "$INPUT_FILE"

# Поиск файлов с обработкой пробелов в именах
find /path -name "*.log" -print0 | while IFS= read -r -d '' file; do
    echo "Found: $file"
done

Антипаттерн: небезопасные переменные

# ПЛОХО: пробелы сломают команду
FILE=/path with spaces/file.txt
rm $FILE

# ПЛОХО: glob expansion
rm $UNTRUSTED_VAR

# ХОРОШО: всегда квотируйте
FILE="/path with spaces/file.txt"
rm "$FILE"

# ХОРОШО: отключите glob если нужно
set -f
rm "$UNTRUSTED_VAR"
set +f

Функции с проверкой зависимостей

require_commands() {
    local missing=()
    for cmd in "$@"; do
        if ! command -v "$cmd" &>/dev/null; then
            missing+=("$cmd")
        fi
    done

    if [[ ${#missing[@]} -gt 0 ]]; then
        echo "Missing commands: ${missing[*]}" >&2
        exit 1
    fi
}

# Использование
require_commands git docker curl jq

Логирование

LOG_LEVEL="${LOG_LEVEL:-INFO}"

log() {
    local level="$1"
    shift
    local msg="$*"

    case "$level" in
        DEBUG) [[ "$LOG_LEVEL" != "DEBUG" ]] && return ;;
        INFO)  [[ "$LOG_LEVEL" =~ ^(DEBUG|INFO)$ ]] || return ;;
        WARN)  ;;
        ERROR) ;;
    esac

    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $msg" >&2
}

# Использование
log INFO "Starting backup"
log DEBUG "Processing file: $file"
log ERROR "Connection failed"

Обработка ошибок

retry() {
    local max_attempts="$1"
    local delay="$2"
    shift 2
    local attempt=1

    until "$@"; do
        if (( attempt == max_attempts )); then
            echo "Command failed after $max_attempts attempts" >&2
            return 1
        fi
        echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
        sleep "$delay"
        (( attempt++ ))
    done
}

# Использование
retry 3 5 curl -f https://api.example.com/health

Массивы и ассоциативные массивы

# Простой массив
declare -a servers=("web1" "web2" "web3")

for server in "${servers[@]}"; do
    ssh "$server" "uptime"
done

# Ассоциативный массив (Bash 4+)
declare -A config=(
    [db_host]="localhost"
    [db_port]="5432"
    [db_name]="myapp"
)

for key in "${!config[@]}"; do
    echo "$key = ${config[$key]}"
done

Полезные конструкции

# Проверка доступности хоста
wait_for_host() {
    local host="$1"
    local timeout="${2:-30}"
    local start=$SECONDS

    while ! ping -c1 -W1 "$host" &>/dev/null; do
        if (( SECONDS - start > timeout )); then
            return 1
        fi
        sleep 1
    done
    return 0
}

Вывод

Надежные Bash-скрипты требуют строгого режима, проверки зависимостей, квотирования переменных и обработки ошибок. Следуйте этим паттернам, и ваши скрипты будут работать предсказуемо в продакшене.