Bash-скрипты вместо рутины: 7 сценариев, которые экономят время каждый день
Практические скрипты для автоматизации бэкапов, мониторинга процессов, массового переименования файлов и деплоя — с разбором каждой строки.
Содержание
Bash-скрипты вместо рутины: 7 сценариев, которые экономят время каждый день
Каждый системный администратор или разработчик рано или поздно замечает: значительная часть рабочего дня уходит на одни и те же действия. Проверить, не упал ли процесс. Сделать бэкап перед деплоем. Переименовать двести файлов по новому шаблону. Отправить отчёт на почту. Всё это — задачи, которые человек выполняет хуже машины: медленнее, с ошибками и с раздражением.
Bash существует с 1989 года и по сей день остаётся главным инструментом автоматизации в Unix-мире. Не потому что он красив или современен, а потому что он есть везде: на любом Linux-сервере, в macOS, в WSL под Windows. Скрипт, написанный сегодня, будет работать на продакшн-сервере через десять лет без единого изменения.
В этой статье — семь конкретных сценариев с разбором кода. Не абстрактные примеры из учебника, а задачи, которые возникают в реальной работе.
Прежде чем начать: несколько базовых принципов
Хороший bash-скрипт — это не просто набор команд, склеенных в файл. Есть несколько практик, которые отделяют рабочий инструмент от хрупкой поделки.
Shebang и строгий режим. Каждый скрипт должен начинаться с:
#!/usr/bin/env bash
set -euo pipefail
set -e останавливает выполнение при любой ошибке. set -u запрещает использование необъявленных переменных. set -o pipefail гарантирует, что ошибка в середине пайпа не будет проглочена. Без этих трёх строк скрипт может продолжать работу после сбоя и натворить дел.
Логирование. Скрипт, который молчит, — это чёрный ящик. Даже простая функция вывода с временной меткой делает отладку в разы проще:
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
Проверка зависимостей. Если скрипт требует rsync, jq или curl, стоит проверить их наличие в начале, а не падать с непонятной ошибкой посередине работы.
for cmd in rsync curl jq; do
command -v "$cmd" &>/dev/null || { echo "Не найдена команда: $cmd"; exit 1; }
done
Сценарий 1: Автоматический бэкап с ротацией
Самая классическая задача. Нужно каждую ночь архивировать директорию, хранить последние семь копий и удалять старые.
#!/usr/bin/env bash
set -euo pipefail
BACKUP_SRC="/var/www/myapp"
BACKUP_DST="/mnt/backups/myapp"
KEEP_DAYS=7
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
ARCHIVE_NAME="backup_${TIMESTAMP}.tar.gz"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
mkdir -p "$BACKUP_DST"
log "Создаём архив: $ARCHIVE_NAME"
tar -czf "${BACKUP_DST}/${ARCHIVE_NAME}" -C "$(dirname "$BACKUP_SRC")" "$(basename "$BACKUP_SRC")"
log "Удаляем бэкапы старше $KEEP_DAYS дней"
find "$BACKUP_DST" -name "backup_*.tar.gz" -mtime +"$KEEP_DAYS" -delete
log "Готово. Текущие бэкапы:"
ls -lh "$BACKUP_DST"
Что здесь важно: tar -C меняет рабочую директорию перед архивацией, поэтому внутри архива не будет абсолютных путей вроде /var/www/myapp/... — только myapp/.... Это упрощает восстановление. find -mtime +7 находит файлы, изменённые более семи дней назад, и удаляет их. Скрипт добавляется в cron одной строкой: 0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1.
Сценарий 2: Мониторинг процесса и автоперезапуск
Процесс упал — нужно его поднять и записать событие в лог. Простой watchdog без лишних зависимостей.
#!/usr/bin/env bash
set -euo pipefail
PROCESS_NAME="myapp"
START_CMD="/opt/myapp/bin/start.sh"
LOG_FILE="/var/log/watchdog.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
if pgrep -x "$PROCESS_NAME" > /dev/null; then
log "Процесс $PROCESS_NAME работает нормально"
else
log "ВНИМАНИЕ: процесс $PROCESS_NAME не найден. Перезапускаем..."
$START_CMD
sleep 3
if pgrep -x "$PROCESS_NAME" > /dev/null; then
log "Процесс успешно перезапущен"
else
log "ОШИБКА: не удалось запустить $PROCESS_NAME"
# Здесь можно добавить отправку алерта
exit 1
fi
fi
pgrep -x ищет процесс по точному имени (флаг -x исключает частичные совпадения). tee -a одновременно выводит сообщение в терминал и дописывает его в лог-файл. Скрипт запускается через cron каждые пять минут: */5 * * * * /opt/scripts/watchdog.sh.
Сценарий 3: Массовое переименование файлов
Фотограф сдал 300 файлов с именами вроде IMG_4521.jpg. Нужно переименовать их в photo_001.jpg, photo_002.jpg и так далее.
#!/usr/bin/env bash
set -euo pipefail
SOURCE_DIR="${1:-.}" # Директория как аргумент, по умолчанию текущая
PREFIX="${2:-photo}"
EXTENSION="${3:-jpg}"
COUNTER=1
for file in "$SOURCE_DIR"/*."$EXTENSION"; do
# Пропускаем, если файлов с таким расширением нет
[[ -e "$file" ]] || continue
NEW_NAME="${SOURCE_DIR}/${PREFIX}_$(printf '%03d' "$COUNTER").${EXTENSION}"
echo "Переименовываем: $(basename "$file") -> $(basename "$NEW_NAME")"
mv "$file" "$NEW_NAME"
((COUNTER++))
done
echo "Готово. Переименовано файлов: $((COUNTER - 1))"
Несколько нюансов. ${1:-.} — это подстановка с умолчанием: если первый аргумент не передан, используется . (текущая директория). printf '%03d' форматирует число с ведущими нулями: 001, 002, 099. Конструкция [[ -e "$file" ]] || continue защищает от ситуации, когда glob не находит файлов и передаёт в цикл буквальную строку *.jpg.
Перед запуском на реальных данных разумно добавить флаг --dry-run: заменить mv на echo mv и проверить, что скрипт делает именно то, что ожидается.
Сценарий 4: Проверка доступности сайтов
Нужно раз в минуту проверять список URL и отправлять уведомление в Telegram, если что-то недоступно.
#!/usr/bin/env bash
set -euo pipefail
SITES=(
"https://example.com"
"https://api.example.com/health"
"https://staging.example.com"
)
TG_BOT_TOKEN="ваш_токен"
TG_CHAT_ID="ваш_chat_id"
send_alert() {
local message="$1"
curl -s -X POST "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" \
-d "chat_id=${TG_CHAT_ID}" \
-d "text=${message}" > /dev/null
}
for url in "${SITES[@]}"; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url")
if [[ "$HTTP_CODE" -ne 200 ]]; then
MESSAGE="⚠️ Сайт недоступен: $url (код: $HTTP_CODE) — $(date '+%H:%M:%S')"
echo "$MESSAGE"
send_alert "$MESSAGE"
else
echo "OK [$HTTP_CODE]: $url"
fi
done
curl -s -o /dev/null -w "%{http_code}" — классический приём: подавляем вывод тела ответа (-o /dev/null), но получаем HTTP-статус через формат вывода (-w). --max-time 10 гарантирует, что зависший сервер не заблокирует скрипт на неопределённое время. Массив SITES с отдельным перебором через "${SITES[@]}" — правильный способ работы с массивами в bash, который корректно обрабатывает пробелы в элементах.
Сценарий 5: Деплой приложения с откатом
Деплой без возможности отката — это хождение по канату без страховки. Вот минималистичный скрипт, который делает снапшот текущей версии перед обновлением.
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/opt/myapp"
BACKUP_DIR="/opt/myapp_backups"
REPO_URL="git@github.com:company/myapp.git"
BRANCH="${1:-main}"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# Создаём снапшот текущей версии
SNAPSHOT="${BACKUP_DIR}/snapshot_$(date '+%Y%m%d_%H%M%S')"
log "Создаём снапшот: $SNAPSHOT"
cp -r "$APP_DIR" "$SNAPSHOT"
# Функция отката — вызывается при ошибке
rollback() {
log "ОШИБКА при деплое. Откатываемся к предыдущей версии..."
rm -rf "$APP_DIR"
cp -r "$SNAPSHOT" "$APP_DIR"
log "Откат выполнен"
exit 1
}
trap rollback ERR
# Деплой
log "Получаем обновления из ветки $BRANCH"
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
log "Устанавливаем зависимости"
npm ci --production
log "Перезапускаем сервис"
systemctl restart myapp
log "Деплой завершён успешно"
Ключевой момент — trap rollback ERR. Это ловушка: при любой ошибке (ненулевой код возврата) bash автоматически вызовет функцию rollback. Это работает в связке с set -e. Такой подход гораздо надёжнее, чем ручная проверка if/else после каждой команды.
Тем, кто хочет разобраться с подобными конструкциями системно — от базового синтаксиса до написания полноценных инструментов автоматизации, — может пригодиться курс «Изучаем bash»: там эти механизмы разбираются последовательно, с практическими заданиями.
Сценарий 6: Анализ логов и генерация отчёта
Нужно каждое утро получать сводку по вчерашним ошибкам из nginx-лога: сколько было 4xx и 5xx, какие URL встречались чаще всего.
#!/usr/bin/env bash
set -euo pipefail
LOG_FILE="/var/log/nginx/access.log"
YESTERDAY=$(date -d "yesterday" '+%d/%b/%Y')
REPORT_FILE="/tmp/nginx_report_$(date '+%Y%m%d').txt"
{
echo "=== Отчёт по логам nginx за $YESTERDAY ==="
echo ""
echo "--- Коды ответов ---"
grep "$YESTERDAY" "$LOG_FILE" | \
awk '{print $9}' | \
sort | uniq -c | sort -rn
echo ""
echo "--- Топ-10 URL с ошибками 4xx ---"
grep "$YESTERDAY" "$LOG_FILE" | \
awk '$9 ~ /^4/ {print $7}' | \
sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Топ-10 URL с ошибками 5xx ---"
grep "$YESTERDAY" "$LOG_FILE" | \
awk '$9 ~ /^5/ {print $7}' | \
sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Топ-10 IP по количеству запросов ---"
grep "$YESTERDAY" "$LOG_FILE" | \
awk '{print $1}' | \
sort | uniq -c | sort -rn | head -10
} > "$REPORT_FILE"
cat "$REPORT_FILE"
# mail -s "Nginx отчёт за $YESTERDAY" ops@company.com < "$REPORT_FILE"
Здесь вся работа построена на классической связке grep | awk | sort | uniq -c | sort -rn. awk '{print $9}' извлекает девятое поле — HTTP-статус в стандартном формате nginx. $9 ~ /^4/ — это регулярное выражение внутри awk: поле начинается с цифры 4. Фигурные скобки вокруг всего блока с перенаправлением > "$REPORT_FILE" — удобный способ записать вывод нескольких команд в один файл без повторения редиректа.
Сценарий 7: Очистка дискового пространства
Диск заполнился — нужно быстро найти виновников и убрать мусор.
#!/usr/bin/env bash
set -euo pipefail
LOG_DIR="/var/log"
TMP_DIR="/tmp"
MAX_LOG_AGE=30 # дней
MIN_FREE_PERCENT=20
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
# Проверяем текущее состояние диска
check_disk() {
local partition="${1:-/}"
local used_percent
used_percent=$(df "$partition" | awk 'NR==2 {gsub(/%/, "", $5); print $5}')
echo "$used_percent"
}
USED=$(check_disk "/")
FREE=$((100 - USED))
log "Использовано: ${USED}%, свободно: ${FREE}%"
if [[ "$FREE" -gt "$MIN_FREE_PERCENT" ]]; then
log "Места достаточно, очистка не требуется"
exit 0
fi
log "Свободного места меньше ${MIN_FREE_PERCENT}%. Начинаем очистку..."
# Удаляем старые логи
log "Удаляем логи старше $MAX_LOG_AGE дней в $LOG_DIR"
find "$LOG_DIR" -name "*.log.*" -mtime +"$MAX_LOG_AGE" -delete
find "$LOG_DIR" -name "*.gz" -mtime +"$MAX_LOG_AGE" -delete
# Очищаем /tmp
log "Очищаем файлы в $TMP_DIR старше 7 дней"
find "$TMP_DIR" -mtime +7 -delete 2>/dev/null || true
# Очищаем кэш пакетного менеджера
if command -v apt-get &>/dev/null; then
log "Очищаем кэш apt"
apt-get clean -y
fi
USED_AFTER=$(check_disk "/")
log "После очистки использовано: ${USED_AFTER}% (было: ${USED}%)"
Обратите внимание на || true после второго find. Команда может завершиться с ошибкой, если у неё нет прав на удаление некоторых файлов в /tmp — это нормально. || true предотвращает срабатывание set -e в этом конкретном месте. Функция check_disk использует awk с gsub для удаления символа % из вывода df перед арифметическим сравнением — без этого bash не сможет сравнить строку "45%" с числом.
Типичные ошибки, которые ломают скрипты
Пробелы в именах файлов. Конструкция for file in $FILES без кавычек сломается на первом же файле с пробелом в имени. Всегда используйте "$variable" и "${array[@]}".
Игнорирование кодов возврата. Без set -e скрипт продолжит работу после неудачной команды. Это особенно опасно в деплой-скриптах.
Хардкод паролей и токенов. Секреты должны храниться в переменных окружения или в файлах с ограниченными правами, но никак не в теле скрипта.
Отсутствие блокировки при параллельном запуске. Если cron запускает скрипт каждую минуту, а он выполняется две минуты, возникнут два параллельных экземпляра. Решается через flock:
exec 9>/var/lock/myscript.lock
flock -n 9 || { echo "Скрипт уже запущен"; exit 1; }
Неочищаемые временные файлы. Используйте trap для гарантированной очистки:
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
Вывод
Bash не требует изучения новых языков, установки интерпретаторов или настройки окружения. Это инструмент, который уже есть там, где нужно работать. Семь сценариев из этой статьи покрывают большую часть ежедневной рутины: резервное копирование, мониторинг, переименование, деплой, анализ логов, очистка диска.
Главное — писать скрипты с умом: строгий режим с самого начала, логирование, защита от типичных ошибок. Скрипт, написанный однажды правильно, работает годами без вмешательства.
Если хочется выстроить понимание bash системно — от синтаксиса и работы с переменными до написания полноценных инструментов, — курс «Изучаем bash» даёт именно такую последовательную базу с практикой на реальных задачах.
Комментарии
Пока нет комментариев