Home » Выполнение команд в shell-скриптах — запуск внешних программ
Выполнение команд в shell-скриптах — запуск внешних программ

Выполнение команд в shell-скриптах — запуск внешних программ

Каждый админ рано или поздно сталкивается с необходимостью запускать внешние программы из shell-скриптов. Это основа автоматизации — от простых утилит до сложных систем деплоя. Понимание того, как правильно запускать и контролировать внешние процессы, поможет вам создавать надёжные скрипты для обслуживания серверов и автоматизации рутинных задач.

Основы запуска внешних программ в shell

В shell-скриптах запуск внешних программ — это основной способ выполнения большинства задач. Когда вы пишете команду в скрипте, shell создаёт дочерний процесс, выполняет программу и возвращает её статус завершения.

#!/bin/bash

# Простой запуск команды
ls -la

# Запуск с сохранением вывода
output=$(ls -la)
echo "Результат: $output"

# Проверка успешности выполнения
if ls /nonexistent 2>/dev/null; then
    echo "Каталог существует"
else
    echo "Каталог не найден"
fi

Ключевые механизмы работы:

  • Прямой запуск — команда выполняется синхронно, скрипт ждёт завершения
  • Захват вывода — через подстановку команд $(command) или обратные кавычки
  • Перенаправление потоков — stdout, stderr, stdin
  • Код возврата — переменная $? содержит статус последней команды

Способы запуска команд

Существует несколько методов выполнения внешних программ, каждый имеет свои особенности:

#!/bin/bash

# Метод 1: Прямой запуск
date

# Метод 2: Подстановка команд (современный синтаксис)
current_date=$(date)
echo "Текущая дата: $current_date"

# Метод 3: Обратные кавычки (устаревший, но всё ещё работает)
old_style=`date`

# Метод 4: Фоновый запуск
sleep 10 &
background_pid=$!
echo "Запущен процесс с PID: $background_pid"

# Метод 5: Через eval (осторожно с безопасностью!)
cmd="ls -la"
eval $cmd
Метод Синтаксис Плюсы Минусы
Прямой запуск command Простота, читаемость Нельзя захватить вывод
$(command) var=$(command) Вложенность, современный синтаксис Теряется код возврата
Обратные кавычки var=`command` Совместимость со старыми системами Устаревший, плохая вложенность
Фоновый запуск command & Параллельное выполнение Сложность контроля

Управление выводом и ошибками

Правильная обработка stdout и stderr — критически важна для надёжных скриптов:

#!/bin/bash

# Перенаправление stdout
ls > output.txt

# Перенаправление stderr
ls /nonexistent 2> error.txt

# Объединение stdout и stderr
ls / /nonexistent > all_output.txt 2>&1

# Подавление вывода
ls /nonexistent 2>/dev/null

# Сохранение и stdout, и stderr отдельно
{
    output=$(ls /home 2>&1)
    exit_code=$?
} 2>/dev/null

# Функция для логирования
log_command() {
    local cmd="$1"
    echo "[$(date)] Выполняется: $cmd" >> /var/log/script.log
    eval "$cmd" >> /var/log/script.log 2>&1
    local status=$?
    echo "[$(date)] Завершено со статусом: $status" >> /var/log/script.log
    return $status
}

# Использование
log_command "systemctl status nginx"

Проверка успешности выполнения

Контроль за выполнением команд — основа надёжности скриптов:

#!/bin/bash

# Включение строгого режима
set -euo pipefail  # завершать при ошибке, неопределённых переменных, ошибках в pipeline

# Простая проверка
if ping -c 1 google.com >/dev/null 2>&1; then
    echo "Интернет доступен"
else
    echo "Нет соединения с интернетом"
    exit 1
fi

# Проверка с детальным анализом
check_service() {
    local service=$1
    if systemctl is-active --quiet "$service"; then
        echo "✓ $service запущен"
        return 0
    else
        echo "✗ $service не запущен"
        return 1
    fi
}

# Функция с retry-логикой
retry_command() {
    local cmd="$1"
    local max_attempts=3
    local delay=2
    
    for ((i=1; i<=max_attempts; i++)); do
        echo "Попытка $i из $max_attempts"
        if eval "$cmd"; then
            echo "Команда выполнена успешно"
            return 0
        else
            echo "Ошибка выполнения команды"
            if [[ $i -lt $max_attempts ]]; then
                echo "Ожидание $delay секунд..."
                sleep $delay
            fi
        fi
    done
    
    echo "Команда не выполнена после $max_attempts попыток"
    return 1
}

# Использование
retry_command "wget -O /tmp/test.html https://example.com"

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

Для высокопроизводительных скриптов важно уметь запускать команды параллельно:

#!/bin/bash

# Простой параллельный запуск
backup_database() {
    echo "Бэкап базы данных..."
    sleep 5
    echo "Бэкап БД завершён"
}

backup_files() {
    echo "Бэкап файлов..."
    sleep 3
    echo "Бэкап файлов завершён"
}

# Запуск в фоне
backup_database &
db_pid=$!

backup_files &
files_pid=$!

# Ожидание завершения всех процессов
wait $db_pid
wait $files_pid

echo "Все бэкапы завершены"

# Использование GNU parallel (если установлен)
check_servers() {
    local servers=("web1.example.com" "web2.example.com" "web3.example.com")
    
    # Последовательно
    for server in "${servers[@]}"; do
        ping -c 1 "$server" >/dev/null && echo "$server: OK" || echo "$server: FAIL"
    done
    
    # Параллельно с parallel
    printf '%s\n' "${servers[@]}" | parallel -j 10 'ping -c 1 {} >/dev/null && echo "{}: OK" || echo "{}: FAIL"'
}

# Пул воркеров
worker_pool() {
    local max_jobs=5
    local job_count=0
    
    for task in task1 task2 task3 task4 task5 task6 task7 task8; do
        while [[ $job_count -ge $max_jobs ]]; do
            wait -n  # ждём завершения любого фонового процесса
            ((job_count--))
        done
        
        (
            echo "Обработка $task"
            sleep $((RANDOM % 5 + 1))
            echo "$task завершён"
        ) &
        
        ((job_count++))
    done
    
    wait  # ждём завершения всех задач
}

Полезные утилиты и команды

Набор инструментов, которые часто используются в администрировании:

#!/bin/bash

# Мониторинг системы
system_check() {
    echo "=== Системная информация ==="
    uptime
    df -h
    free -h
    
    echo -e "\n=== Топ процессов ==="
    ps aux --sort=-%cpu | head -10
    
    echo -e "\n=== Сетевые соединения ==="
    ss -tuln
    
    echo -e "\n=== Дисковые операции ==="
    iostat -x 1 1
}

# Управление сервисами
service_manager() {
    local services=("nginx" "mysql" "redis" "php7.4-fpm")
    
    for service in "${services[@]}"; do
        if systemctl is-enabled --quiet "$service"; then
            status=$(systemctl is-active "$service")
            echo "$service: $status"
            
            if [[ $status != "active" ]]; then
                echo "Перезапуск $service..."
                systemctl restart "$service"
            fi
        fi
    done
}

# Очистка логов
cleanup_logs() {
    local log_dirs=("/var/log/nginx" "/var/log/mysql" "/var/log/php")
    
    for log_dir in "${log_dirs[@]}"; do
        if [[ -d $log_dir ]]; then
            echo "Очистка логов в $log_dir"
            find "$log_dir" -name "*.log" -type f -mtime +30 -delete
            find "$log_dir" -name "*.gz" -type f -mtime +90 -delete
        fi
    done
}

# Проверка безопасности
security_check() {
    echo "=== Проверка безопасности ==="
    
    # Проверка неудачных попыток входа
    echo "Последние неудачные попытки входа:"
    grep "Failed password" /var/log/auth.log | tail -5
    
    # Проверка открытых портов
    echo -e "\nОткрытые порты:"
    nmap -sT -O localhost
    
    # Проверка обновлений
    echo -e "\nДоступные обновления:"
    apt list --upgradable 2>/dev/null || yum check-update
}

Интеграция с внешними API

Современные скрипты часто взаимодействуют с веб-сервисами:

#!/bin/bash

# Функция для HTTP-запросов
http_request() {
    local method=$1
    local url=$2
    local data=$3
    local headers=$4
    
    curl -s -X "$method" \
         -H "Content-Type: application/json" \
         -H "$headers" \
         -d "$data" \
         "$url"
}

# Отправка уведомлений в Slack
notify_slack() {
    local message=$1
    local webhook_url="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
    
    local payload=$(cat </dev/null | \
                 openssl x509 -noout -enddate | cut -d= -f2)
        
        expiry_timestamp=$(date -d "$expiry" +%s)
        current_timestamp=$(date +%s)
        days_left=$(( (expiry_timestamp - current_timestamp) / 86400 ))
        
        if [[ $days_left -lt 30 ]]; then
            notify_slack "⚠️ SSL-сертификат для $domain истекает через $days_left дней!"
        fi
    done
}

# Мониторинг веб-сайтов
monitor_websites() {
    local sites=("https://example.com" "https://api.example.com/health")
    
    for site in "${sites[@]}"; do
        response_time=$(curl -o /dev/null -s -w "%{time_total}" "$site")
        http_code=$(curl -o /dev/null -s -w "%{http_code}" "$site")
        
        if [[ $http_code -ne 200 ]]; then
            notify_slack "🔴 Сайт $site недоступен (HTTP $http_code)"
        elif (( $(echo "$response_time > 5.0" | bc -l) )); then
            notify_slack "🟡 Сайт $site медленно отвечает (${response_time}s)"
        fi
    done
}

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

Профессиональные скрипты должны корректно обрабатывать ошибки:

#!/bin/bash

# Настройка отладки
set -euo pipefail
# set -x  # раскомментировать для отладки

# Глобальные переменные
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="/var/log/$(basename "$0" .sh).log"
PID_FILE="/var/run/$(basename "$0" .sh).pid"

# Функция логирования
log() {
    local level=$1
    shift
    local message="$*"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}

# Функция очистки при завершении
cleanup() {
    log "INFO" "Завершение скрипта"
    [[ -f $PID_FILE ]] && rm -f "$PID_FILE"
    # Завершение фоновых процессов
    jobs -p | xargs -r kill 2>/dev/null || true
}

# Обработчик сигналов
trap cleanup EXIT INT TERM

# Проверка блокировки
check_lock() {
    if [[ -f $PID_FILE ]]; then
        local old_pid=$(cat "$PID_FILE")
        if kill -0 "$old_pid" 2>/dev/null; then
            log "ERROR" "Скрипт уже запущен с PID $old_pid"
            exit 1
        else
            log "WARN" "Найден устаревший PID-файл, удаляю"
            rm -f "$PID_FILE"
        fi
    fi
    echo $$ > "$PID_FILE"
}

# Безопасное выполнение команд
safe_exec() {
    local cmd="$1"
    local description="$2"
    
    log "INFO" "Выполняется: $description"
    log "DEBUG" "Команда: $cmd"
    
    if eval "$cmd"; then
        log "INFO" "Успешно: $description"
        return 0
    else
        local exit_code=$?
        log "ERROR" "Ошибка при выполнении: $description (код: $exit_code)"
        return $exit_code
    fi
}

# Проверка зависимостей
check_dependencies() {
    local deps=("curl" "jq" "systemctl" "nginx")
    local missing=()
    
    for dep in "${deps[@]}"; do
        if ! command -v "$dep" >/dev/null 2>&1; then
            missing+=("$dep")
        fi
    done
    
    if [[ ${#missing[@]} -gt 0 ]]; then
        log "ERROR" "Не найдены зависимости: ${missing[*]}"
        exit 1
    fi
}

# Основная функция
main() {
    log "INFO" "Запуск скрипта"
    
    check_lock
    check_dependencies
    
    # Ваш код здесь
    safe_exec "nginx -t" "Проверка конфигурации nginx"
    safe_exec "systemctl reload nginx" "Перезагрузка nginx"
    
    log "INFO" "Скрипт завершён успешно"
}

# Запуск только если скрипт выполняется напрямую
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Практические примеры для серверного администрирования

Вот несколько готовых скриптов для повседневных задач:

#!/bin/bash

# Автоматическое резервное копирование
backup_script() {
    local backup_dir="/backup/$(date +%Y%m%d)"
    local mysql_user="backup_user"
    local mysql_pass="backup_password"
    
    mkdir -p "$backup_dir"
    
    # Бэкап базы данных
    mysqldump -u "$mysql_user" -p"$mysql_pass" --all-databases \
        | gzip > "$backup_dir/mysql_backup.sql.gz"
    
    # Бэкап файлов сайта
    tar -czf "$backup_dir/website_backup.tar.gz" -C /var/www/html .
    
    # Очистка старых бэкапов (старше 7 дней)
    find /backup -type d -mtime +7 -exec rm -rf {} \; 2>/dev/null || true
    
    # Отправка на удалённый сервер
    rsync -av "$backup_dir/" backup@remote.server.com:/backups/
}

# Мониторинг дискового пространства
disk_monitor() {
    local threshold=80
    
    df -h | awk 'NR>1 {print $5 " " $1 " " $6}' | while read line; do
        usage=$(echo "$line" | awk '{print $1}' | sed 's/%//')
        partition=$(echo "$line" | awk '{print $2}')
        mountpoint=$(echo "$line" | awk '{print $3}')
        
        if [[ $usage -gt $threshold ]]; then
            message="⚠️ Диск заполнен на ${usage}%: $partition ($mountpoint)"
            echo "$message"
            # Отправка уведомления
            notify_slack "$message"
        fi
    done
}

# Обновление системы
system_update() {
    log "INFO" "Начинаю обновление системы"
    
    # Проверка доступности репозиториев
    if ! apt-get update; then
        log "ERROR" "Не удалось обновить список пакетов"
        return 1
    fi
    
    # Получение списка обновлений
    local updates=$(apt list --upgradable 2>/dev/null | wc -l)
    
    if [[ $updates -gt 1 ]]; then
        log "INFO" "Доступно обновлений: $((updates-1))"
        
        # Обновление пакетов
        DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
        
        # Проверка необходимости перезагрузки
        if [[ -f /var/run/reboot-required ]]; then
            log "WARN" "Требуется перезагрузка системы"
            notify_slack "🔄 Сервер требует перезагрузки после обновления"
        fi
    else
        log "INFO" "Система актуальна"
    fi
}

# Очистка временных файлов
cleanup_temp() {
    local temp_dirs=("/tmp" "/var/tmp" "/var/cache/apt/archives")
    
    for dir in "${temp_dirs[@]}"; do
        if [[ -d $dir ]]; then
            log "INFO" "Очистка $dir"
            
            # Удаление файлов старше 3 дней
            find "$dir" -type f -mtime +3 -delete 2>/dev/null || true
            
            # Удаление пустых директорий
            find "$dir" -type d -empty -delete 2>/dev/null || true
        fi
    done
    
    # Очистка логов systemd
    journalctl --vacuum-time=30d
    
    # Очистка кэша пакетов
    apt-get autoclean
    apt-get autoremove -y
}

Интеграция с системами мониторинга

Современные инструменты мониторинга требуют интеграции на уровне скриптов:

#!/bin/bash

# Отправка метрик в Prometheus
send_metrics() {
    local metric_name=$1
    local metric_value=$2
    local labels=$3
    
    # Используем node_exporter textfile collector
    local metrics_file="/var/lib/node_exporter/textfile_collector/custom_metrics.prom"
    
    echo "# HELP $metric_name Custom metric from script" > "$metrics_file"
    echo "# TYPE $metric_name gauge" >> "$metrics_file"
    echo "${metric_name}{${labels}} $metric_value" >> "$metrics_file"
}

# Интеграция с Grafana через API
grafana_annotation() {
    local title=$1
    local text=$2
    local grafana_url="http://grafana.example.com:3000"
    local api_key="your_api_key_here"
    
    local payload=$(cat < 80" | bc -l) )); then
        grafana_annotation "High CPU Usage" "CPU usage is ${cpu_usage}%"
    fi
    
    if (( $(echo "$memory_usage > 85" | bc -l) )); then
        grafana_annotation "High Memory Usage" "Memory usage is ${memory_usage}%"
    fi
}

Безопасность и лучшие практики

При работе с внешними командами критически важно соблюдать принципы безопасности:

  • Валидация входных данных — всегда проверяйте пользовательский ввод
  • Избегайте eval — используйте только для доверенных данных
  • Используйте полные пути — избегайте PATH injection
  • Установка строгих прав — скрипты должны иметь минимальные права
  • Логирование действий — все операции должны быть задокументированы
#!/bin/bash

# Безопасная обработка пользовательского ввода
validate_input() {
    local input=$1
    local pattern=$2
    
    if [[ ! $input =~ $pattern ]]; then
        log "ERROR" "Недопустимый ввод: $input"
        return 1
    fi
}

# Безопасный запуск команд с пользовательскими данными
safe_user_command() {
    local user_input=$1
    
    # Валидация — только буквы, цифры, точки и дефисы
    if ! validate_input "$user_input" '^[a-zA-Z0-9.-]+$'; then
        return 1
    fi
    
    # Использование -- для разделения опций и аргументов
    /usr/bin/systemctl status -- "$user_input"
}

# Установка безопасных переменных окружения
secure_environment() {
    # Очистка PATH
    export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
    
    # Установка безопасного umask
    umask 077
    
    # Очистка опасных переменных
    unset IFS
    export LC_ALL=C
}

Заключение

Умение правильно запускать внешние программы в shell-скриптах — это основа эффективного администрирования серверов. Освоив эти техники, вы сможете создавать надёжные системы автоматизации, которые будут работать стабильно и безопасно.

Основные принципы, которые стоит помнить:

  • Всегда проверяйте коды возврата команд
  • Используйте строгий режим (set -euo pipefail) для отлова ошибок
  • Логируйте все важные операции
  • Валидируйте входные данные
  • Предусматривайте обработку исключительных ситуаций

Для серьёзных проектов рекомендую арендовать VPS или выделенный сервер для тестирования и развёртывания ваших скриптов. Это позволит вам экспериментировать с различными техниками в безопасной среде.

Дополнительные ресурсы для изучения:


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked