Создание bash-скриптов: переменные, условия, циклы и отладка

Почему bash-скрипты остаются незаменимыми даже в эпоху облаков и Docker

Несмотря на развитие контейнеризации, автоматизации и DevOps-инструментов, старый добрый Bash продолжает быть ядром многих задач системного администрирования и CI/CD процессов. Причина проста: его доступность, скорость исполнения и тесная интеграция с Unix-средой. Но, как показывает практика, большинство скриптов страдают от одних и тех же проблем — непереносимости, неустойчивости и слабой читаемости. В этой статье мы разберёмся, как написать надёжный и масштабируемый bash-скрипт, используя переменные, условия, циклы и техники отладки.

Переменные: больше, чем просто контейнеры для данных

Неочевидные подводные камни

Переменные в Bash по умолчанию — строки. Это часто приводит к неожиданным результатам при арифметике или сравнении:

```bash
a="01"
b="1"
[[ $a == $b ]] && echo "равны" || echo "не равны" # Выведет: не равны
```

Причина — строковое сравнение. Чтобы сравнивать числа, используйте двойные круглые скобки:

```bash
(( a == b )) && echo "равны" || echo "не равны"
```

Лайфхаки с переменными

- Используйте `${VAR:-default}` для безопасной подстановки значений по умолчанию.
- Очищайте переменные с `unset VAR`, а не `VAR=""`, если хотите полностью удалить их из окружения.
- Для счётчиков используйте `let`, `(( ))` или встроенную арифметику:

```bash
count=0
((count++))
```

Условия: логика с нюансами

Классика жанра с twist’ом

Операторы `if`, `[[ ]]`, `[ ]` и `(( ))` — похожи, но имеют ключевые отличия. Например, `[[ ]]` более надёжен при работе со строками, особенно содержащими пробелы или спецсимволы:

```bash
file="my file.txt"
[[ -f "$file" ]] # безопасно
[ -f "$file" ] # может дать ошибку
```

Альтернативный подход: case

Для сложных ветвлений `case` может быть чище, чем вложенные `if`:

```bash
case "$1" in
start) systemctl start app ;;
stop) systemctl stop app ;;
*) echo "Usage: $0 {start|stop}" ;;
esac
```

Этот шаблон легко расширяется и читается гораздо лучше, чем каскад `if-elif-else`.

Циклы: когда `for` и `while` работают, а когда нет

Ошибка новичка: парсинг вывода

Цикл по `for i in $(ls)` может сломать скрипт, если в именах файлов есть пробелы. Правильный способ — использовать `find -print0` и `xargs -0`, либо `while read` с уточнёнными IFS:

```bash
while IFS= read -r -d '' file; do
echo "Обработка файла: $file"
done < <(find . -type f -print0) ```

Полезные паттерны

- Бесконечные циклы с контролем выхода:

```bash
while true; do
check_something && break
sleep 1
done
```

- Итерации по массиву:

```bash
arr=("foo" "bar" "baz")
for item in "${arr[@]}"; do
echo "$item"
done
```

Отладка: от `echo` до `trap`

Невидимые враги скрипта

Ошибки часто кроются не в логике, а в окружении: неправильный путь, пустая переменная, неявный `exit`. Решение — включить строгий режим:

```bash
set -euo pipefail
IFS=$'nt'
```

Это заставит скрипт падать при любой ошибке, что облегчает отладку на раннем этапе.

Инструменты профессионала

- `bash -x script.sh` — пошаговая трассировка команд.
- `trap` — перехват сигнала и отладка при выходе:

```bash
trap 'echo "Ошибка в строке $LINENO"; exit 1' ERR
```

- Логирование в STDERR:

```bash
log() { echo "[LOG] $*" >&2; }
```

Реальный пример: резервное копирование с проверкой

Ниже — пример скрипта, который делает резервную копию, проверяет успешность и отправляет уведомление:

```bash
#!/bin/bash
set -euo pipefail
trap 'echo "Ошибка на строке $LINENO"; exit 1' ERR

SRC="/var/www"
DEST="/backup/$(date +%F)"
LOG="/var/log/backup.log"

mkdir -p "$DEST"

rsync -a "$SRC/" "$DEST/" >> "$LOG" 2>&1

if [[ $? -eq 0 ]]; then
echo "Резервное копирование прошла успешно: $DEST" | mail -s "Backup OK" admin@example.com
else
echo "Ошибка при резервном копировании" | mail -s "Backup FAILED" admin@example.com
fi
```

Здесь применены:
- Проверка ошибок через `set -e`
- Переменные с датой
- Условие с `if`
- Логирование и уведомление

Бонус: 5 трюков, которые знают только опытные

- Используйте `declare -A` для ассоциативных массивов (Bash 4+).
- Разделяйте скрипты на функции и храните в отдельных модулях.
- Проверяйте наличие бинарников через `command -v` или `type`.
- Используйте `getopts` для обработки флагов и параметров.
- Замените `echo` на `printf` — он надёжнее при спецсимволах.

Заключение: пишите скрипты, которыми не стыдно делиться

Создание скриптов — это не просто набор команд, а полноценное программирование с учётом UX, читаемости и отказоустойчивости. Потратьте время на структуру, отладку и обработку ошибок — и ваш Bash станет инструментом, которому доверяют. Даже в мире Kubernetes и Ansible он по-прежнему на своём месте.

4
5
Прокрутить вверх