Скрипт, который при ошибке останавливается, убирает за собой и говорит, что случилось
trapbash -xПо умолчанию bash, встретив ошибку, пишет сообщение и выполняет следующую строку. Для интерактивной работы это удобно, для скрипта — опасно:
#!/bin/bash
cd /nonexistent/backup
echo "удаляю старые копии в $(pwd)"
$ bash noe.sh
noe.sh: строка 2: cd: /nonexistent/backup: Нет такого файла или каталога
удаляю старые копии в /home/student
Будь вместо echo команда rm -rf *, она удалила бы содержимое домашнего каталога. В модуле 03 мы ставили || exit 1 после опасных команд; строгий режим делает это для всех сразу.
Вторая строка почти каждого рабочего скрипта:
#!/bin/bash
set -euo pipefail
| Опция | Что делает | От чего защищает |
|---|---|---|
-e | Остановить скрипт, если команда завершилась ошибкой | Продолжение после неудачного шага |
-u | Ошибка при обращении к незаданной переменной | Опечатки в именах: $bakup_dir вместо $backup_dir |
-o pipefail | Код конвейера — код последней упавшей команды, а не последней в цепочке | Ошибки в середине a | b | c |
$ bash e.sh # тот же скрипт с set -euo pipefail
e.sh: строка 3: cd: /nonexistent/backup: Нет такого файла или каталога
$ echo $?
1
Опечатка в имени переменной без -u превращает rm -rf "$bakup_dir/"* в rm -rf /*. С -u:
$ bash u.sh
u.sh: строка 4: bakup_dir: не заданы границы переменной
А без pipefail конвейер «успешен», даже если его начало упало:
grep ERROR /nonexistent.log | wc -l # код 0 — wc отработал
set -o pipefail
grep ERROR /nonexistent.log | wc -l # код 2 — от grep
Строгий режим — страховочная сетка с дырами. Их нужно знать, чтобы не думать «у меня же set -e».
#!/bin/bash
set -euo pipefail
if grep -q nobody_xyz /etc/passwd; then echo найден; else echo "не найден — скрипт продолжает"; fi
count=$(grep -c nobody_xyz /etc/passwd || true)
echo "count=$count"
f() { local out=$(false); echo "local скрыл ошибку"; }
f
n=0
(( n++ ))
echo "сюда не дойдём"
$ bash pit.sh; echo "код $?"
не найден — скрипт продолжает
count=0
local скрыл ошибку
код 1
| Случай | Что происходит | Как быть |
|---|---|---|
Команда в условии if, while, перед && / || | Ошибка — это ответ «нет», скрипт продолжает. Так и задумано | Пользоваться этим: grep -q в if |
grep ничего не нашёл | Код 1 остановит скрипт | || true, если «не найдено» — нормальный исход |
local x=$(cmd) | Код local (успех) скрывает ошибку cmd | Раздельно: local x; x=$(cmd) — ShellCheck предупредит (SC2155) |
(( n++ )) при n = 0 | Выражение равно 0 — код 1, скрипт остановится | n=$((n + 1)) или (( ++n )) |
[[ -n $x ]] && cmd последней строкой функции | При пустом $x функция вернёт 1 — и это станет кодом скрипта, если функция вызвана в trap … EXIT | if [[ -n $x ]]; then cmd; fi |
trap 'команды' СОБЫТИЕ выполняет команды при событии. EXIT срабатывает при любом завершении скрипта — успешном, по ошибке set -e, по exit, по Ctrl+C. Это место для удаления временных файлов и снятия блокировок.
#!/bin/bash
set -euo pipefail
tmp=$(mktemp)
trap 'rm -f "$tmp"; echo "удалён $tmp" >&2' EXIT
echo "работаю с $tmp"
date > "$tmp"
false # ошибка: скрипт остановится
echo "не выполнится"
$ bash tr.sh
работаю с /tmp/tmp.QRItpyxVyX
удалён /tmp/tmp.QRItpyxVyX
mktemp создаёт файл с уникальным случайным именем и правами 600, mktemp -d — каталог. Не придумывайте имена вида /tmp/backup.tmp: два запуска скрипта затрут файлы друг друга, а предсказуемое имя в общем /tmp — известный способ атаки через символическую ссылку.
ERR срабатывает при ошибке команды — удобно, чтобы сообщить, где именно упал скрипт. Внутри функций ловушка ERR по умолчанию не действует; чтобы действовала, к строгому режиму добавляют -E: set -Eeuo pipefail.
trap 'echo "ошибка в строке $LINENO: $BASH_COMMAND (код $?)" >&2' ERR
$ bash err.sh
начало
cp: невозможно открыть '/etc/shadow' для чтения: Отказано в доступе
ошибка в строке 5: cp /etc/shadow /tmp/x (код 1)
Скрипт, работающий по расписанию, никто не видит. Всё, что он делал, должно остаться в журнале с датой и уровнем важности.
log_file=/var/log/backup.log
log() { printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "${*:2}" | tee -a "$log_file" >&2; }
log INFO "запуск"
log ERROR "нет места на диске"
$ cat /var/log/backup.log
2026-10-14 22:05:34 [INFO] запуск
2026-10-14 22:05:34 [ERROR] нет места на диске
Другой вариант — писать в общий журнал systemd командой logger: не нужно думать о правах на файл и его ротации, а читать можно через journalctl с фильтрами по времени:
$ logger -t backup "проверка записи в журнал"
$ sudo journalctl -t backup -n 1
окт 14 22:05:34 debian backup[954277]: проверка записи в журнал
-t — метка, по которой потом ищут; -p err — уровень важности. Файл журнала в /var/log со временем растёт — для него настраивают ротацию (/etc/logrotate.d/), журнал systemd ограничен сам.
Резервное копирование запускается каждый час и однажды идёт дольше часа. Второй экземпляр стартует поверх первого, оба пишут в одни файлы, диск забивается. Защита — блокировка файлом через flock:
#!/bin/bash
set -euo pipefail
exec 9>/run/lock/backup.lock # открыть файл блокировки на дескрипторе 9
if ! flock -n 9; then
echo "уже запущен другой экземпляр, выхожу" >&2
exit 1
fi
echo "работаю (PID $$)"
sleep 3
$ bash lock.sh & sleep 0.5; bash lock.sh
работаю (PID 954270)
уже запущен другой экземпляр, выхожу
Блокировку снимает ядро, когда процесс завершается любым способом, даже по kill -9, — в отличие от самодельного «файла-флага», который останется после аварии и заблокирует все следующие запуски.
Внешние команды, которые могут зависнуть (сеть, удалённые диски), ограничивают по времени: timeout 300 rsync … завершит команду через 5 минут с кодом 124.
bash -n скрипт проверяет синтаксис, ничего не выполняя. bash -x скрипт выполняет и печатает каждую команду после подстановок — видно, какие значения на самом деле получили переменные:
$ bash -n bad.sh
bad.sh: строка 3: синтаксическая ошибка: неожиданный конец файла
$ PS4='+ ${LINENO}: ' bash -x dbg.sh
+ 2: name=мир
+ 3: for i in 1 2
+ 4: echo 'привет, мир (1)'
привет, мир (1)
+ 3: for i in 1 2
+ 4: echo 'привет, мир (2)'
привет, мир (2)
PS4 задаёт префикс строк трассировки — номер строки помогает найти место. Трассировку можно включить для части скрипта: set -x … set +x.
set -x в скриптах, которые работают с секретами, и не выкладывайте их вывод в чаты и задачи.#!/bin/bash
# имя — что делает. Запуск: имя [-n] АРГУМЕНТЫ
set -Eeuo pipefail
readonly LOG_TAG=имя
log() { logger -t "$LOG_TAG" -p "user.$1" -- "${*:2}"; echo "[$1] ${*:2}" >&2; }
die() { log err "$*"; exit 1; }
exec 9>"/run/lock/$LOG_TAG.lock"
flock -n 9 || die "уже запущен"
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
trap 'log err "строка $LINENO: $BASH_COMMAND"' ERR
log info "старт"
# … работа …
log info "готово"
Последнее свойство надёжного скрипта — идемпотентность: повторный запуск после сбоя доводит работу до конца, а не ломает сделанное. mkdir -p вместо mkdir, замена строки вместо дописывания, проверка «уже сделано?» перед каждым шагом, который нельзя повторять.
set -euo pipefail — во второй строке; помните исключения: условия, local x=$(…), (( n++ ))mktemp, удаление — в trap … EXITtee -a или в journald через logger -tflock -n — защита от двойного запуска, timeout — от зависанияbash -n — синтаксис, bash -x с PS4 — трассировка; секреты в трассировку не пускать