Поймать коды ошибок в оболочке - PullRequest
85 голосов
/ 11 октября 2009

У меня сейчас есть скрипт, который делает что-то вроде

./a | ./b | ./c

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

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

Ответы [ 4 ]

140 голосов
/ 10 февраля 2011

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

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

38 голосов
/ 07 февраля 2012

Вы также можете проверить массив ${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

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

19 голосов
/ 11 октября 2009

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

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 (когда оболочка выходит по любой причине) , Если вы пишете сценарий оболочки, окончательная ловушка должна только удалить ловушку на 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 не помогли.

8 голосов
/ 18 июня 2016

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

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

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 файлом, если вам необходимо сохранить вывод последней команды в любом месте.

Чтобы больше поиграть с этой конструкцией и убедить себя, что это действительно делает то, что должно, я заменил ./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
...