← Весь справочник

Коды возврата

$?, цепочки && и ||, set -e/-u/-o pipefail, exit и trap — как скрипт узнаёт об ошибках

Каждая команда, завершаясь, возвращает число от 0 до 255. 0 — успех, любое другое — ошибка или «нет». На этом коде держатся if, &&, ||, while и все проверки в скриптах.

Синтаксис

команда; echo $?             # код последней команды
команда1 && команда2         # вторая — только если первая успешна
команда1 || команда2         # вторая — только если первая упала
exit N                       # выйти из скрипта с кодом N
return N                     # выйти из функции с кодом N

Частые коды

КодЧто обычно значит
0успех
1общая ошибка; у grep — «ничего не найдено», у diff — «есть различия»
2неправильное использование: неверная опция, синтаксическая ошибка; у grep — ошибка чтения
124timeout прервал команду по времени
126файл найден, но не исполняемый (нет x)
127команда не найдена (опечатка, нет в PATH)
128+Nпроцесс убит сигналом N
130128+2: прерван Ctrl+C (SIGINT)
137128+9: убит SIGKILL — часто это OOM killer
143128+15: завершён SIGTERM
255exit -1 или код вне диапазона; у ssh — ошибка соединения

Код берётся по модулю 256: exit 300 даёт 44.

Операторы и переменные

ЗаписьЧто делает
$?код последней команды; перезаписывается каждой командой, включая echo и [[ ]]
a && bb, если a вернула 0
a || bb, если a вернула не 0
a && b || cc выполнится, если упадёт a или b — это не if/else
! командаинвертировать код: 0 → 1, не-0 → 0
${PIPESTATUS[@]}коды всех частей последнего конвейера
rc=$?сохранить код сразу, пока его не перезаписали
команда || true«ошибка не важна» — код всегда 0
exit без числавыйти с кодом последней команды

Опции set

ОпцияЧто делает
set -e (-o errexit)выйти из скрипта, как только команда вернёт не 0 (с исключениями, см. ниже)
set -u (-o nounset)обращение к незаданной переменной — ошибка и выход
set -o pipefailконвейер падает, если упала любая его часть, а не только последняя
set -x (-o xtrace)печатать каждую команду перед выполнением (с префиксом +) — отладка
set -E (-o errtrace)trap на ERR срабатывает и внутри функций
set +e, set +x …выключить опцию
set -euo pipefailчастое начало «строгого» скрипта
set -oпоказать состояние всех опций

set -e не срабатывает, если команда стоит: в условии if/while/until, слева от && или ||, после !, внутри конвейера (кроме последней части, если нет pipefail).

trap

ЗаписьЧто делает
trap 'команды' EXITвыполнить при любом выходе из скрипта: обычном, по exit, по set -e
trap 'команды' ERRвыполнить, когда команда вернула не 0 (там же, где сработал бы set -e)
trap 'команды' INT TERMперехватить Ctrl+C и kill
trap '' INTигнорировать сигнал
trap - INTвернуть обработку по умолчанию
trap -pпоказать установленные обработчики

SIGKILL (9) и SIGSTOP перехватить нельзя.

Примеры

# узнать код
ls /nope 2>/dev/null; echo $?          # 2
grep -q zzz /etc/passwd; echo $?       # 1 — не найдено

# выполнить, только если предыдущее удалось
mkdir -p /tmp/build && cd /tmp/build && echo "готово"

# запасной вариант при ошибке
cd /some/dir || exit 1

# сохранить код и разобрать его
some_command
rc=$?
if (( rc != 0 )); then echo "упало с кодом $rc" >&2; fi

# проверить успех и сохранить вывод
if ! out=$(ls /nope 2>&1); then
    echo "ошибка: $out" >&2
fi

# строгий режим в начале скрипта
#!/bin/bash
set -euo pipefail

# при set -u — значение по умолчанию для необязательной переменной
echo "${DEBUG:-0}"

# удалить временный файл при любом выходе
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT

# сообщить, где упал скрипт
set -eE
trap 'echo "ошибка в строке $LINENO" >&2' ERR

# код возврата при убийстве сигналом
sleep 100 & kill $!; wait $!; echo $?  # 143

Подводные камни

  • $? живёт одну команду. cmd; echo "статус"; echo $? покажет код echo. Сохраняй rc=$? сразу.
  • a && b || c — не if/else. Если a успешна, а b упала, выполнится и c. Для настоящего ветвления пиши if.
  • set -e не ловит ошибки в условиях. Функция, вызванная как f || echo fail, выполняется целиком без errexit: упавшие команды внутри неё не остановят выполнение.
  • local x=$(cmd) и export x=$(cmd) прячут ошибку: код — от local/export, то есть 0. Разделяй: local x; x=$(cmd).
  • ((i++)) при i=0 возвращает 1, и set -e завершит скрипт. Пиши ((i+=1)) или i=$((i+1)).
  • Без pipefail ошибка в середине конвейера теряется: false | true — успех.
  • exit в подоболочке выходит только из неё. ( exit 1 ) или cmd | while …; do exit 1; done скрипт не завершат.
  • trap на сигнал без exit не завершает скрипт. trap 'echo stop' TERM перехватит kill, и скрипт продолжит работу. Добавляй exit в обработчик.

См. также

conditions-loops · redirection · shellcheck · kill