← Все статьи
Bash: паттерны и антипаттерны скриптов
Строгий режим
Каждый серьезный скрипт должен начинаться с 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-скрипты требуют строгого режима, проверки зависимостей, квотирования переменных и обработки ошибок. Следуйте этим паттернам, и ваши скрипты будут работать предсказуемо в продакшене.