Вопрос:

Поймать коды ошибок в оболочке

shell error-handling pipe

31926 просмотра

4 ответа

51815 Репутация автора

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

./a | ./b | ./c

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

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

Автор: hugomg Источник Размещён: 11.10.2009 03:17

Ответы (4)


19 плюса

600717 Репутация автора

Решение

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

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', поэтому я использовал эту однозначную запись для надежности целую вечность.

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

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 не помогли.

Автор: Jonathan Leffler Размещён: 11.10.2009 03:40

144 плюса

3017 Репутация автора

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

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

Автор: Michel Samia Размещён: 10.02.2011 04:13

40 плюса

572 Репутация автора

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

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

Автор: Imron Размещён: 06.02.2012 10:50

8 плюса

4082 Репутация автора

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

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

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
Автор: josch Размещён: 18.06.2016 06:12
Вопросы из категории :
32x32