Bash-скрипт сообщил об успехе после ошибки: код возврата и pipefail

Ночной скрипт выгружает данные и передаёт поток следующей программе. Утром задача отмечена успешной, хотя выгрузка не состоялась. В журнале даже есть сообщение исходной программы об ошибке. Причина часто оказывается в том, что shell получил статус последней команды конвейера, а она корректно обработала пустой вход.

У каждой завершившейся команды есть код возврата. Ноль обычно означает успех, ненулевое значение — иной исход согласно контракту программы. В Bash значение $? относится к только что выполненной команде. Оно не хранит историю всех процессов, которые участвовали в конвейере.

Воспроизведём потерянную ошибку

Для опыта не нужны внешние сервисы и файлы. Первый процесс отправляет сообщение в stderr и завершается с кодом 7. Второй читает stdout и считает строки. Сообщение об ошибке в канал данных не попадает:

#!/usr/bin/env bash
set +e
set +o pipefail
bash -c 'printf "source failed\n" >&2; exit 7' | wc -l
printf 'without pipefail: %s\n' "$?"

set -o pipefail
bash -c 'printf "source failed\n" >&2; exit 7' | wc -l
printf 'with pipefail: %s\n' "$?"

Оба запуска wc выводят ноль строк. Статусы конвейеров различаются: сначала 0, затем 7. Сообщение source failed появляется в stderr в обоих случаях. Именно поэтому наличие красной строки в логе ещё не означает, что планировщик увидел неуспешное завершение задачи.

Опция pipefail выбирает статус крайней справа команды, завершившейся с ненулевым кодом. Если все команды успешны, результат остаётся нулевым. Это правило не выбирает первую ошибку по времени и не останавливает уже запущенные процессы конвейера.

Когда нужен статус каждого участника

При диагностике полезно узнать, сломался источник данных или обработчик. Для этого Bash предоставляет массив PIPESTATUS. Сохраните его сразу: даже последующий printf меняет состояние, которое вы хотели прочитать.

set +e
set -o pipefail
bash -c 'exit 7' | wc -l
statuses=("${PIPESTATUS[@]}")
printf 'source=%s consumer=%s\n' "${statuses[0]}" "${statuses[1]}"

Ожидаются статусы источника 7 и получателя 0. Промежуточное присваивание rc=$? перед сохранением массива тоже было бы командой. Поэтому не пытайтесь сначала выполнить несколько диагностических действий, а потом восстановить исходные статусы. Сначала снимок, затем его интерпретация.

Выразим действие после ошибки явно

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

#!/usr/bin/env bash
set -o pipefail
fetch_data() { printf 'upstream unavailable\n' >&2; return 7; }

if fetch_data | wc -l; then
  printf 'pipeline completed\n'
else
  rc=$?
  printf 'pipeline failed: %s\n' "$rc" >&2
  exit "$rc"
fi

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

Популярная настройка set -e полезна, но имеет исключения. В частности, неуспешная команда в условии if не заставляет shell немедленно выйти; здесь результат должен разбирать сам автор. Условия, списки с && и ||, инверсия через ! также меняют контекст. Не объясняйте поведение скрипта обещанием «любая ошибка всё остановит».

Ненулевой код бывает ожидаемым

grep возвращает 1, если совпадений нет. Для поиска необязательной записи это нормальный исход задачи, хотя для shell код ненулевой. У grep код 2 означает уже ошибку. Обрабатывайте различие там, где знаете смысл поиска; универсальное || true поглотит оба случая.

Есть и конвейеры, где получатель намеренно читает только начало потока и закрывает канал. Источник может получить SIGPIPE. С включённым pipefail это проявится как неуспех. Решение зависит от контракта: нужен ли весь поток или достаточно первых строк. После исправления проверьте обычный ответ, ошибку источника и ожидаемый пустой результат. Тогда статус задания будет соответствовать выполненной работе.

Ещё по теме

Продолжить изучение

Хотите перейти от отдельных команд к сопровождению Linux-серверов? Работа с системой, скриптами и сервисами входит в программу «Linux и Bash для DevOps-инженеров».

Продолжить на практике

  • Linux и Bash для DevOps-инженеров ↗

    Linux для работы с серверами: процессы, права, systemd, сеть, диски и логи. Автоматизируйте повседневные задачи на Bash и разберите типичные сбои сервиса — от занятого порта до переполненного диска.