Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Bash — это не «просто командная строка». Это полноценный скриптовый язык, на котором написаны тысячи скриптов деплоя, мониторинга, бэкапов и автоматизации по всему миру. Проблема в том, что большинство bash-скриптов написаны наспех, без понимания подводных камней, и падают в самый неподходящий момент. Эта статья о том, как писать bash-скрипты, которым можно доверять.


Основы: заголовок, который должен быть в каждом скрипте

#!/usr/bin/env bash
# Описание: что делает скрипт
# Автор: your@email.com
# Версия: 1.0.0

set -euo pipefail
IFS=$'\n\t'

Разберём set -euo pipefail — это не магия, это защита:

  • set -e — скрипт останавливается при ошибке любой команды

  • set -u — ошибка при обращении к неустановленной переменной

  • set -o pipefail — ошибка в пайпе не скрывается (без этого false | true вернёт 0)

  • IFS=$'\n\t' — разделитель полей только перевод строки и таб, не пробел

Без этих строк скрипт будет молча продолжаться после ошибок, что приводит к катастрофическим последствиям на продакшне.


Работа с переменными и параметрами

Правильное использование переменных

# Всегда кавычки вокруг переменных!
name="John Doe"
echo "$name"        # правильно
echo $name          # сломается если пробел в имени

# Значения по умолчанию
database="${DB_NAME:-myapp_production}"
timeout="${TIMEOUT:-30}"

# Проверка обязательного параметра
: "${API_KEY:?'API_KEY is required but not set'}"

# Массивы
servers=("web01" "web02" "web03")
for server in "${servers[@]}"; do
    echo "Processing $server"
done

# Ассоциативные массивы (bash 4+)
declare -A ports
ports[web]=80
ports[mysql]=3306
ports[redis]=6379

for service in "${!ports[@]}"; do
    echo "$service: ${ports[$service]}"
done

Обработка аргументов командной строки

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

# Продвинутая обработка аргументов с getopts
usage() {
    cat <<EOF
Usage: $(basename "$0") [OPTIONS] <environment>

Options:
  -h, --help        Show this help
  -v, --verbose     Verbose output
  -n, --dry-run     Dry run, don't make changes
  -c, --config FILE Config file path (default: /etc/myapp/config.conf)

Environment:
  staging    Deploy to staging
  production Deploy to production

Examples:
  $(basename "$0") -v staging
  $(basename "$0") --dry-run production
EOF
    exit 1
}

# Разбор длинных опций через getopt
OPTS=$(getopt -o hvnc: --long help,verbose,dry-run,config: -n "$(basename "$0")" -- "$@")
eval set -- "$OPTS"

VERBOSE=false
DRY_RUN=false
CONFIG="/etc/myapp/config.conf"

while true; do
    case "$1" in
        -h|--help) usage ;;
        -v|--verbose) VERBOSE=true; shift ;;
        -n|--dry-run) DRY_RUN=true; shift ;;
        -c|--config) CONFIG="$2"; shift 2 ;;
        --) shift; break ;;
        *) echo "Unknown option: $1"; usage ;;
    esac
done

ENVIRONMENT="${1:-}"
[[ -z "$ENVIRONMENT" ]] && { echo "Error: environment required"; usage; }
[[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]] && {
    echo "Error: environment must be 'staging' or 'production'"
    exit 1
}

$VERBOSE && echo "Config: $CONFIG"
$VERBOSE && echo "Environment: $ENVIRONMENT"
$DRY_RUN && echo "DRY RUN MODE - no changes will be made"

Логирование — правильный подход

# Цвета для терминала
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly NC='\033[0m'  # No Color

# Лог-файл
LOG_FILE="/var/log/myapp/deploy-$(date +%Y%m%d-%H%M%S).log"
mkdir -p "$(dirname "$LOG_FILE")"

log() {
    local level="$1"
    shift
    local message="$*"
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    # В файл — без цветов
    echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
    
    # В терминал — с цветами
    case "$level" in
        INFO)  echo -e "${GREEN}[INFO]${NC}  $message" ;;
        WARN)  echo -e "${YELLOW}[WARN]${NC}  $message" ;;
        ERROR) echo -e "${RED}[ERROR]${NC} $message" >&2 ;;
        DEBUG) $VERBOSE && echo -e "${BLUE}[DEBUG]${NC} $message" ;;
    esac
}

# Использование
log INFO "Starting deployment to $ENVIRONMENT"
log WARN "Skipping health check in dry-run mode"
log ERROR "Failed to connect to database"
log DEBUG "Connection string: $DB_URL"

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

trap — всегда убирай за собой

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

# Временные файлы
TEMP_DIR=$(mktemp -d)
LOCK_FILE="/tmp/myapp.lock"

# Функция очистки — выполняется при любом выходе
cleanup() {
    local exit_code=$?
    
    log INFO "Cleaning up..."
    rm -rf "$TEMP_DIR"
    rm -f "$LOCK_FILE"
    
    if [[ $exit_code -ne 0 ]]; then
        log ERROR "Script failed with exit code $exit_code"
        # Отправляем уведомление
        send_alert "Deployment failed on $(hostname)" "$LOG_FILE"
    fi
    
    exit $exit_code
}

# Обработка сигналов
error_handler() {
    local line_number="$1"
    log ERROR "Error on line $line_number"
}

trap cleanup EXIT
trap 'error_handler $LINENO' ERR
trap 'log WARN "Interrupted by user"; exit 130' INT TERM

# Защита от параллельного запуска через lockfile
acquire_lock() {
    if ! mkdir "$LOCK_FILE" 2>/dev/null; then
        local pid
        pid=$(cat "$LOCK_FILE/pid" 2>/dev/null || echo "unknown")
        log ERROR "Another instance is running (PID: $pid)"
        exit 1
    fi
    echo $$ > "$LOCK_FILE/pid"
    log DEBUG "Lock acquired"
}

acquire_lock

Retry-логика для нестабильных операций

# Универсальная функция retry
retry() {
    local max_attempts="$1"
    local delay="$2"
    local description="$3"
    shift 3
    local cmd=("$@")
    
    local attempt=1
    while true; do
        log INFO "[$attempt/$max_attempts] Trying: $description"
        
        if "${cmd[@]}"; then
            log INFO "Success: $description"
            return 0
        fi
        
        if [[ $attempt -ge $max_attempts ]]; then
            log ERROR "All $max_attempts attempts failed: $description"
            return 1
        fi
        
        log WARN "Attempt $attempt failed, retrying in ${delay}s..."
        sleep "$delay"
        ((attempt++))
        
        # Экспоненциальная задержка (удваиваем каждый раз, макс 60 сек)
        delay=$(( delay * 2 > 60 ? 60 : delay * 2 ))
    done
}

# Использование
retry 5 2 "Health check" curl -sf http://localhost/health
retry 3 5 "Database backup" pg_dump myapp > /backup/dump.sql

Параллельное выполнение задач

# Простой способ — запуск в background + wait
deploy_to_servers() {
    local servers=("$@")
    local pids=()
    local failed=0
    
    for server in "${servers[@]}"; do
        log INFO "Starting deploy on $server"
        (
            ssh "$server" 'cd /app && git pull && systemctl restart myapp'
            log INFO "Deploy done on $server"
        ) &
        pids+=($!)
    done
    
    # Ждём все процессы и собираем статусы
    for i in "${!pids[@]}"; do
        if ! wait "${pids[$i]}"; then
            log ERROR "Deploy failed on ${servers[$i]}"
            ((failed++))
        fi
    done
    
    return $failed
}

# С ограничением параллелизма (не более N одновременно)
run_parallel() {
    local max_jobs="$1"
    shift
    local items=("$@")
    
    local running=0
    local pids=()
    
    for item in "${items[@]}"; do
        # Ждём если достигнут лимит
        while [[ $running -ge $max_jobs ]]; do
            # Проверяем завершившиеся процессы
            local new_pids=()
            for pid in "${pids[@]}"; do
                if kill -0 "$pid" 2>/dev/null; then
                    new_pids+=("$pid")
                else
                    ((running--))
                fi
            done
            pids=("${new_pids[@]}")
            sleep 0.1
        done
        
        process_item "$item" &
        pids+=($!)
        ((running++))
    done
    
    wait
}

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

Безопасное чтение конфигурации

# Загрузка .env файла
load_env() {
    local env_file="${1:-.env}"
    
    [[ -f "$env_file" ]] || { log WARN "No $env_file found"; return 0; }
    
    # Безопасная загрузка: только KEY=VALUE строки, без выполнения кода
    while IFS='=' read -r key value; do
        # Пропускаем комментарии и пустые строки
        [[ "$key" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$key" ]] && continue
        
        # Удаляем пробелы вокруг ключа
        key="${key//[[:space:]]/}"
        
        # Удаляем кавычки из значения
        value="${value%\"}"
        value="${value#\"}"
        value="${value%\'}"
        value="${value#\'}"
        
        # Экспортируем только валидные имена переменных
        if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
            export "$key=$value"
        fi
    done < "$env_file"
}

Манипуляции со строками

# Встроенные операции (без fork, быстрее)
string="Hello, World!"

# Длина
echo "${#string}"  # 13

# Подстрока (offset:length)
echo "${string:7:5}"  # World

# Замена (первое вхождение)
echo "${string/Hello/Hi}"  # Hi, World!

# Замена (все вхождения)
path="/usr/local/bin:/usr/bin:/bin"
echo "${path//:/ }"  # /usr/local/bin /usr/bin /bin

# Upper/Lower case (bash 4+)
echo "${string^^}"  # HELLO, WORLD!
echo "${string,,}"  # hello, world!

# Trim (удаление пробелов)
trim() {
    local str="$*"
    str="${str#"${str%%[![:space:]]*}"}"
    str="${str%"${str##*[![:space:]]}"}"
    echo "$str"
}

# Разбивка строки на массив
IFS=',' read -ra parts <<< "one,two,three"
for part in "${parts[@]}"; do echo "$part"; done

Реальные примеры: скрипты для продакшна

Скрипт деплоя с zero-downtime

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

DEPLOY_DIR="/var/www/myapp"
RELEASES_DIR="$DEPLOY_DIR/releases"
CURRENT_LINK="$DEPLOY_DIR/current"
SHARED_DIR="$DEPLOY_DIR/shared"
KEEP_RELEASES=5

deploy() {
    local release_dir
    release_dir="$RELEASES_DIR/$(date +%Y%m%d%H%M%S)"
    
    log INFO "Creating release directory: $release_dir"
    mkdir -p "$release_dir"
    
    # Клонируем или обновляем код
    log INFO "Deploying code..."
    if [[ -d "$DEPLOY_DIR/repo" ]]; then
        git -C "$DEPLOY_DIR/repo" fetch origin
        git -C "$DEPLOY_DIR/repo" archive HEAD | tar -x -C "$release_dir"
    else
        git clone --depth 1 "$GIT_REPO" "$DEPLOY_DIR/repo"
        git -C "$DEPLOY_DIR/repo" archive HEAD | tar -x -C "$release_dir"
    fi
    
    # Линкуем shared директории
    ln -sfn "$SHARED_DIR/uploads" "$release_dir/public/uploads"
    ln -sfn "$SHARED_DIR/.env" "$release_dir/.env"
    
    # Устанавливаем зависимости
    log INFO "Installing dependencies..."
    composer install --no-dev --optimize-autoloader -d "$release_dir"
    
    # Прогреваем кэш
    log INFO "Warming up cache..."
    php "$release_dir/spark" cache:clear
    
    # Переключаем симлинк атомарно
    log INFO "Switching to new release..."
    ln -sfn "$release_dir" "${CURRENT_LINK}.new"
    mv -Tf "${CURRENT_LINK}.new" "$CURRENT_LINK"
    
    # Перезагружаем PHP-FPM (graceful)
    kill -USR2 $(cat /var/run/php/php8.2-fpm.pid)
    
    # Очищаем старые релизы
    cleanup_old_releases
    
    log INFO "Deploy completed successfully!"
}

cleanup_old_releases() {
    local releases
    releases=$(ls -1t "$RELEASES_DIR")
    local count
    count=$(echo "$releases" | wc -l)
    
    if [[ $count -gt $KEEP_RELEASES ]]; then
        echo "$releases" | tail -n +"$((KEEP_RELEASES + 1))" | while read -r release; do
            log INFO "Removing old release: $release"
            rm -rf "$RELEASES_DIR/$release"
        done
    fi
}

rollback() {
    local previous
    previous=$(ls -1t "$RELEASES_DIR" | sed -n '2p')
    
    if [[ -z "$previous" ]]; then
        log ERROR "No previous release to rollback to"
        exit 1
    fi
    
    log INFO "Rolling back to: $previous"
    ln -sfn "$RELEASES_DIR/$previous" "${CURRENT_LINK}.rollback"
    mv -Tf "${CURRENT_LINK}.rollback" "$CURRENT_LINK"
    kill -USR2 $(cat /var/run/php/php8.2-fpm.pid)
    log INFO "Rollback completed"
}

case "${1:-deploy}" in
    deploy) deploy ;;
    rollback) rollback ;;
    *) echo "Usage: $0 [deploy|rollback]"; exit 1 ;;
esac

Скрипт мониторинга и уведомлений

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

# Пороговые значения
CPU_THRESHOLD=85
MEM_THRESHOLD=90
DISK_THRESHOLD=85
LOAD_THRESHOLD=4.0

ALERT_EMAIL="ops@example.com"
WEBHOOK_URL="${SLACK_WEBHOOK:-}"

send_alert() {
    local subject="$1"
    local body="$2"
    
    # Email
    echo "$body" | mail -s "[$HOSTNAME] ALERT: $subject" "$ALERT_EMAIL"
    
    # Slack webhook
    if [[ -n "$WEBHOOK_URL" ]]; then
        curl -s -X POST "$WEBHOOK_URL" \
            -H 'Content-type: application/json' \
            -d "{\"text\":\":warning: *[$HOSTNAME]* $subject\n\`\`\`$body\`\`\`\"}"
    fi
}

check_cpu() {
    local cpu_usage
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1 | cut -d',' -f1)
    cpu_usage=${cpu_usage%.*}  # целая часть
    
    if [[ $cpu_usage -gt $CPU_THRESHOLD ]]; then
        local top_processes
        top_processes=$(ps aux --sort=-%cpu | head -6 | tail -5)
        send_alert "High CPU: ${cpu_usage}%" "CPU usage: ${cpu_usage}%\n\nTop processes:\n$top_processes"
    fi
}

check_memory() {
    local mem_total mem_used mem_percent
    mem_total=$(free -m | awk '/^Mem:/{print $2}')
    mem_used=$(free -m | awk '/^Mem:/{print $3}')
    mem_percent=$(( mem_used * 100 / mem_total ))
    
    if [[ $mem_percent -gt $MEM_THRESHOLD ]]; then
        local top_processes
        top_processes=$(ps aux --sort=-%mem | head -6 | tail -5)
        send_alert "High Memory: ${mem_percent}%" "Memory: ${mem_used}MB / ${mem_total}MB (${mem_percent}%)\n\n$top_processes"
    fi
}

check_disk() {
    while IFS= read -r line; do
        local usage mount
        usage=$(echo "$line" | awk '{print $5}' | tr -d '%')
        mount=$(echo "$line" | awk '{print $6}')
        
        if [[ $usage -gt $DISK_THRESHOLD ]]; then
            send_alert "High Disk: $mount at ${usage}%" \
                "Disk usage on $mount: ${usage}%\n\n$(df -h "$mount")"
        fi
    done < <(df -h | grep -E '^/dev/' | grep -v tmpfs)
}

check_services() {
    local services=("nginx" "php8.2-fpm" "mysql" "redis")
    
    for service in "${services[@]}"; do
        if ! systemctl is-active --quiet "$service"; then
            send_alert "Service DOWN: $service" \
                "Service $service is not running!\n\n$(systemctl status "$service" --no-pager | tail -20)"
            
            # Попытка перезапуска
            log WARN "Attempting to restart $service..."
            systemctl restart "$service" && \
                send_alert "Service RECOVERED: $service" "Service $service was restarted successfully"
        fi
    done
}

# Запускаем все проверки
check_cpu
check_memory
check_disk
check_services

Лучшие практики

Используйте shellcheck — это ESLint для bash:

apt install shellcheck
shellcheck myscript.sh
# В CI/CD:
find . -name "*.sh" -exec shellcheck {} +

Тестируйте скрипты с bats:

apt install bats
# test.bats:
@test "cleanup removes old files" {
    touch /tmp/testfile
    run cleanup /tmp/testfile
    [ "$status" -eq 0 ]
    [ ! -f /tmp/testfile ]
}
bats test.bats

Документируйте через heredoc:

show_help() {
    cat <<'EOF'
...документация...
EOF
}

Bash — это мощный инструмент, но он требует дисциплины. Скрипт, написанный правильно — это надёжный автоматизированный сотрудник. Написанный небрежно — бомба с часовым механизмом.

User Feedback

Create an account or sign in to leave a review

There are no reviews to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.