Улавливание кодов ошибок в оболочке

В настоящее время у меня есть script, который делает что-то вроде

./a | ./b | ./c

Я хочу изменить его так, чтобы, если какой-либо из b, c или c с кодом ошибки, я печатаю сообщение об ошибке и останавливаю, а не удаляю неудачный вывод вперед.

Каким будет самый простой/самый чистый способ сделать это?

Ответ 1

Если вы действительно не хотите, чтобы вторая команда продолжалась до тех пор, пока первая, как известно, не будет успешной, вам, вероятно, придется использовать временные файлы. Простая версия:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Перенаправление '1 > & 2' также может быть сокращено ' > & 2'; однако старая версия оболочки MKS неправильно перенаправила ошибку без предшествующего "1", поэтому я использовал эту однозначную нотацию для надежности в течение многих лет.

Это утечка файлов, если вы что-то прервите. Бомбардировочное (более или менее) программирование оболочки использует:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

Первая строка ловушки говорит: "Запустите команды" rm -f $tmp.[12]; exit 1 ", когда произойдут какие-либо из сигналов 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE или 15 SIGTERM или 0 (когда оболочка выходит по какой-либо причине), Если вы пишете оболочку script, последней ловушке нужно только удалить ловушку на 0, которая является ловушкой выхода из оболочки (вы можете оставить другие сигналы на месте, так как процесс вот-вот закончится).

В исходном конвейере для 'c' возможно считывание данных с 'b' до завершения 'a' - это обычно желательно (например, для работы нескольких ядер). Если "b" является фазой "сортировки", это не будет применяться - "b" должен видеть все свои данные, прежде чем он сможет сгенерировать любой из своих выходных данных.

Если вы хотите обнаружить, какие команды не выполняются, вы можете использовать:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Это просто и симметрично - тривиально распространяться на конвейер из 4 частей или N-части.

Простые эксперименты с 'set -e' не помогли.

Ответ 2

В bash вы можете использовать set -e и set -o pipefail в начале вашего файла. Последующая команда ./a | ./b | ./c завершится с ошибкой при сбое любого из трех сценариев. Код возврата будет кодом возврата первого сбойного файла script.

Обратите внимание, что pipefail недоступно в стандартном sh.

Ответ 3

Вы также можете проверить массив ${PIPESTATUS[]} после полного выполнения, например. если вы запустите:

./a | ./b | ./c

Затем ${PIPESTATUS} будет массивом кодов ошибок из каждой команды в трубе, поэтому, если средняя команда не удалась, echo ${PIPESTATUS[@]} будет содержать что-то вроде:

0 1 0

и что-то вроде этого запускается после команды:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

позволит вам проверить, что все команды в трубе сработали.

Ответ 4

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

Вот решение, которое ловит все ошибки в трубе, а не только ошибки последнего компонента. Так что это как bash pipefail, просто более мощный в том смысле, что вы можете получить все коды ошибок.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Чтобы обнаружить, что что-то не удалось, команда echo печатает стандартную ошибку в случае сбоя любой команды. Затем объединенный стандартный вывод ошибки сохраняется в $res и исследуется позже. Именно поэтому стандартная ошибка всех процессов перенаправляется на стандартный вывод. Вы также можете отправить этот вывод в /dev/null или оставить его еще одним индикатором того, что что-то пошло не так. Вы можете заменить последнее перенаправление на /dev/null файлом, если yo uneed для сохранения вывода последней команды в любом месте.

Чтобы больше играть с этой конструкцией и убедить себя, что это действительно то, что нужно, я заменил ./a, ./b и ./c на подоболочки, которые выполняют echo, cat и exit. Вы можете использовать это, чтобы проверить, что эта конструкция действительно пересылает все выходные данные из одного процесса в другой и что коды ошибок записываются правильно.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi