Получить статус выхода процесса, который передан другому


Ответы:


256

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

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Если вы используете zsh, они называют массив pipestatus(дело имеет значение!), А индексы массива начинаются с одного:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Чтобы объединить их внутри функции таким образом, чтобы значения не терялись:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Запустите выше в bashили, zshи вы получите те же результаты; только один из retval_bashи retval_zshбудет установлен. Другой будет пустым. Это позволит функции заканчиваться return $retval_bash $retval_zsh(обратите внимание на отсутствие кавычек!).


9
И pipestatusв зш. К сожалению, другие оболочки не имеют этой функции.
Жиль

8
Примечание: массивы в zsh начинаются нелогично с индекса 1, так что это так echo "$pipestatus[1]" "$pipestatus[2]".
Кристоф Вурм

5
Вы можете проверить весь конвейер следующим образом:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

7
@JanHudec: Возможно, вы должны прочитать первые пять слов моего ответа. Также любезно укажите, где вопрос требовал ответа только для POSIX.
Camh

12
@JanHudec: И при этом это не было помечено POSIX. Почему вы предполагаете, что ответ должен быть POSIX? Это не было указано, поэтому я дал квалифицированный ответ. В моем ответе нет ничего неправильного, плюс есть несколько ответов для других случаев.
camh

238

Есть 3 распространенных способа сделать это:

Pipefail

Первый способ , чтобы установить pipefailпараметр ( ksh, zshили bash). Это самое простое, и он в основном устанавливает статус $?выхода для кода завершения последней программы, чтобы он выходил из ненулевого значения (или ноля, если все выходы успешно завершены).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash также имеет переменную-массив $PIPESTATUS( $pipestatusin zsh), которая содержит состояние завершения всех программ в последнем конвейере.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

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

Отдельные казни

Это самое громоздкое из решений. Запустите каждую команду отдельно и сохраните статус

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1

2
Штопать! Я просто собирался написать о PIPESTATUS.
SLM

1
Для справки, в этом вопросе SO обсуждается несколько других техник: stackoverflow.com/questions/1221833/…
slm

1
@ Патрик, решение pipestatus работает на bash, просто больше вопросов, если я использую сценарий ksh, как вы думаете, мы сможем найти что-то похожее на pipestatus? (Между тем я вижу, что pshestatus не поддерживается ksh)
yael

2
@yael Я не использую ksh, но, если взглянуть на справочную страницу , она не поддерживает $PIPESTATUSили что-то подобное. Это поддерживает pipefailопцию, хотя.
Патрик

1
Я решил пойти с pipefail, так как он позволяет мне получить статус LOG=$(failed_command | successful_command)
сбойной

51

Это решение работает без использования специальных функций bash или временных файлов. Бонус: в конце концов, статус выхода на самом деле является статусом выхода, а не какой-то строкой в ​​файле.

Ситуация:

someprog | filter

Вы хотите статус выхода из someprogи выход из filter.

Вот мое решение:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

Результатом этой конструкции является стандартный вывод filterкак стандартный вывод конструкции и статус выхода из someprogсостояния выхода конструкции.


эта конструкция также работает с простой группировкой команд {...}вместо подоболочек (...). Подоболочки имеют некоторые последствия, в том числе затраты на производительность, которые нам здесь не нужны. прочтите подробное руководство по bash для получения более подробной информации: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html.

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

К сожалению, грамматике bash требуются пробелы и точки с запятой для фигурных скобок, так что конструкция становится намного более просторной.

Для остальной части этого текста я буду использовать вариант subshell.


Пример someprogи filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Пример вывода:

filtered line1
filtered line2
filtered line3
42

Примечание: дочерний процесс наследует дескрипторы открытого файла от родительского. Это означает, someprogчто унаследует дескриптор 3 и 4 открытого файла. Если someprogвыполняется запись в дескриптор 3 файла, то это станет статусом выхода. Реальный статус выхода будет проигнорирован, потому что readчитает только один раз.

Если вы беспокоитесь о том, что вы someprogможете записать в файловый дескриптор 3 или 4, то лучше всего закрыть файловые дескрипторы перед вызовом someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

The exec 3>&- 4>&-before someprogзакрывает файловый дескриптор перед выполнением, someprogпоэтому для someprogэтих файловых дескрипторов просто не существует.

Это также можно записать так: someprog 3>&- 4>&-


Пошаговое объяснение конструкции:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

Снизу вверх:

  1. Подоболочка создается с файловым дескриптором 4, перенаправленным на стандартный вывод. Это означает, что все, что будет напечатано в файловом дескрипторе 4 в подоболочке, в конечном итоге станет выводом всей конструкции.
  2. Канал создается, и команды left ( #part3) и right ( #part2) выполняются. exit $xsтакже является последней командой канала, и это означает, что строка из stdin будет статусом выхода всей конструкции.
  3. Подоболочка создается с файловым дескриптором 3, перенаправленным на стандартный вывод. Это означает, что все, что будет напечатано в файловом дескрипторе 3 в этой подоболочке, будет в конечном итоге #part2и, в свою очередь, будет выходным состоянием всей конструкции.
  4. Канал создается, и команды слева ( #part5и #part6) и справа ( filter >&4) выполняются. Выходные данные filterперенаправляются в файловый дескриптор 4. В #part1файловом дескрипторе 4 был перенаправлен на стандартный вывод. Это означает, что выводом filterявляется стандартный вывод всей конструкции.
  5. Статус выхода из #part6распечатывается в файловый дескриптор 3. В #part3файловом дескрипторе 3 был перенаправлен на #part2. Это означает, что статус выхода из #part6будет окончательным статусом выхода для всей конструкции.
  6. someprogвыполнен. Статус выхода принимается #part5. Stdout берется по трубе #part4и пересылается filter. Выход из filterволи в свою очередь достигает стандартного вывода, как объяснено в#part4

1
Приятно. Для функции, которую я мог бы просто сделать(read; exit $REPLY)
Jthill

5
(exec 3>&- 4>&-; someprog)упрощает до someprog 3>&- 4>&-.
Роджер Пэйт

Этот метод также работает без подоболочек:{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
josch

36

Хотя не совсем то, что вы просили, вы могли бы использовать

#!/bin/bash -o pipefail

так что ваши каналы возвращают последний ненулевой возврат.

может быть немного меньше кодирования

Изменить: Пример

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1

9
set -o pipefailвнутри скрипта должен быть более устойчивым, например, в случае, если кто-то выполняет скрипт через bash foo.sh.
maxschlepzig

Как это работает? у тебя есть пример?
Йохан

2
Обратите внимание, что -o pipefailнет в POSIX.
scy

2
Это не работает в моем выпуске BASH 3.2.25 (1). Вверху / tmp / ff у меня есть #!/bin/bash -o pipefail. Ошибка:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Фелипе Альварес

2
@FelipeAlvarez: В некоторых средах (включая Linux) не разобрать пробела на #!линию за пределами первого, и таким образом это становится /bin/bash -o pipefail /tmp/ff, вместо необходимого /bin/bash -o pipefail /tmp/ff- getopt(или аналогичного) разбора использование optarg, который является следующим пунктом в ARGV, в качестве аргумента чтобы -o, так что не получается. Если бы вам нужно было сделать обертку (скажем, bash-pfчто только что сделал exec /bin/bash -o pipefail "$@"и поместить это в #!строку, это сработало бы. См. Также: en.wikipedia.org/wiki/Shebang_%28Unix%29
lindes

22

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

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Или, если я знаю, что вывод fooникогда не содержит строку с просто .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Это всегда можно сделать, если есть какой-то способ заставить barработать все, кроме последней строки, и передать последнюю строку в качестве кода выхода.

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

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

После этого $exit_codesобычно foo:X bar:Y, но это может быть, bar:Y foo:Xесли выйдет, barпрежде чем прочитать все его входные данные или если вам не повезло. Я думаю , что пишет для труб диаметром до 512 байт атомарны на всех юниксы, так foo:$?и bar:$?части не будут смешиваться, пока строки тегов находятся в 507 байт.

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

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

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

  • Если одновременно запущено несколько сценариев или один и тот же сценарий использует этот метод в нескольких местах, необходимо убедиться, что они используют разные временные имена файлов.
  • Надежно создать временный файл в общей папке. Зачастую /tmpэто единственное место, где скрипт наверняка сможет писать файлы. Используйте mktemp, который не является POSIX, но в настоящее время доступен на всех серьезных устройствах.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

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

17

Начиная с конвейера:

foo | bar | baz

Вот общее решение с использованием только оболочки POSIX и без временных файлов:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses содержит коды состояния любых сбойных процессов в произвольном порядке с индексами, указывающими, какая команда отправила каждое состояние.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Обратите внимание на цитаты $error_statusesв моих тестах; без них grepневозможно дифференцироваться, потому что переводы строк приводятся к пробелам.


12

Поэтому я хотел дать ответ, подобный ответу Лесманы, но я думаю, что мой, пожалуй, немного проще и немного более выгодно решение с чистой оболочкой Борна:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Я думаю, это лучше всего объяснить изнутри - command1 выполнит и напечатает свой обычный вывод на stdout (дескриптор файла 1), затем, как только это будет сделано, printf выполнит и напечатает код выхода command1 на своем stdout, но этот stdout перенаправляется на дескриптор файла 3.

Пока команда command1 выполняется, ее стандартный вывод передается в command2 (вывод printf никогда не попадает в command2, потому что мы отправляем его в файловый дескриптор 3 вместо 1, который читает канал). Затем мы перенаправляем вывод command2 в файловый дескриптор 4, чтобы он также не входил в файловый дескриптор 1 - потому что мы хотим, чтобы файловый дескриптор 1 был немного позже, потому что мы перенесем вывод printf из файлового дескриптора 3 обратно в файловый дескриптор 1 - потому что это то, что команда замещения (обратные метки), будет захватывать, и это то, что будет помещено в переменную.

Последнее волшебство заключается в том, что сначала exec 4>&1мы сделали отдельную команду - она ​​открывает дескриптор файла 4 как копию стандартного вывода внешней оболочки. Подстановка команд будет захватывать все, что написано в стандарте, с точки зрения команд внутри него - но, поскольку выходные данные команды 2 собираются в файловом дескрипторе 4, что касается подстановки команд, подстановка команд не захватывает это - однако, как только он «выходит» из подстановки команд, он фактически все еще идет к общему файловому дескриптору скрипта 1.

(Это exec 4>&1должна быть отдельная команда, потому что многим распространенным оболочкам не нравится, когда вы пытаетесь записать в файловый дескриптор внутри подстановки команды, которая открывается во «внешней» команде, использующей подстановку. Так что это Простейший портативный способ сделать это.)

Вы можете посмотреть на это менее технически и более игриво, как если бы результаты команд перепрыгивали друг друга: команда 1 отправляет канал команде 2, затем вывод printf перепрыгивает через команду 2, так что команда 2 не перехватывает ее, а затем выходные данные команды 2 перепрыгивают из подстановки команд так же, как и printf приземляется как раз вовремя, чтобы быть захваченными подстановкой, так что она попадает в переменную, а выходные данные команды 2 продолжают работать, записываясь в стандартный вывод, так же, как в обычной трубе.

Кроме того, насколько я понимаю, он по- $?прежнему будет содержать код возврата второй команды в конвейере, поскольку назначения переменных, подстановки команд и составные команды эффективно прозрачны для кода возврата команды внутри них, поэтому статус возврата command2 должен быть распространен - ​​это, и не нужно определять дополнительную функцию, поэтому я думаю, что это могло бы быть несколько лучшим решением, чем предложенное lesmana.

В соответствии с предостережениями, которые упоминает лесмена, вполне возможно, что в какой-то момент команда1 будет использовать файловые дескрипторы 3 или 4, поэтому для большей надежности вы должны сделать следующее:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Обратите внимание, что в моем примере я использую составные команды, но подоболочки (использование ( )вместо { }будет также работать, хотя, возможно, будет менее эффективным.)

Команды наследуют файловые дескрипторы от процесса, который их запускает, поэтому вся вторая строка наследует файловый дескриптор четыре, а составная команда, за которой следует, 3>&1наследует файловый дескриптор три. Таким образом, команда 4>&-гарантирует, что внутренняя составная команда не унаследует файловый дескриптор четыре, и 3>&-не унаследует файловый дескриптор три, поэтому команда 1 получает «более чистую», более стандартную среду. Вы также можете перемещать внутреннее 4>&-рядом с 3>&-, но я понимаю, почему бы просто не ограничить его область настолько, насколько это возможно.

Я не уверен, как часто вещи используют файловый дескриптор 3 и 4 напрямую - я думаю, что в большинстве случаев программы используют системные вызовы, которые возвращают неиспользуемые в данный момент файловые дескрипторы, но иногда код записывает код непосредственно в файловый дескриптор 3, я предположить (я мог бы представить программу, проверяющую дескриптор файла, чтобы увидеть, открыт ли он, и использующую его, если он есть, или соответствующим образом ведущий себя иначе, если это не так). Поэтому последнее, вероятно, лучше всего учитывать и использовать в случаях общего назначения.


Выглядит интересно, но я не могу понять, что вы ожидаете от этой команды, и мой компьютер тоже не может; Я получаю -bash: 3: Bad file descriptor.
G-Man

@ G-Man Хорошо, я продолжаю забывать, что bash понятия не имеет, что он делает, когда дело доходит до файловых дескрипторов, в отличие от оболочек, которые я обычно использую (пепел, который поставляется с busybox). Я дам вам знать, когда подумаю об обходном пути, который делает Bash счастливым. В то же время, если у вас есть удобный Debian Box, вы можете попробовать его в Dash, или, если у вас есть удобный busybox, вы можете попробовать его с busybox ash / sh.
mtraceur

@ G-Man То, что я ожидаю от команды и что она делает в других оболочках, это перенаправление stdout из command1, поэтому оно не попадает под замену команды, но после выхода из подстановки команды оно падает fd3 возвращается в стандартный вывод, поэтому он передается, как ожидается, command2. Когда команда1 завершается, printf запускает и печатает свой статус выхода, который фиксируется в переменной подстановкой команды. Очень подробная разбивка здесь: stackoverflow.com/questions/985876/tee-and-exit-status/… Кроме того, ваш комментарий читается так, как если бы он был своего рода оскорбительным?
mtraceur

С чего мне начать? (1) Извините, если вы почувствовали себя оскорбленным. «Выглядит интересно» подразумевалось серьезно; было бы замечательно, если бы что-нибудь компактное сработало так, как вы ожидали. Кроме того, я просто говорил, что не понимаю, что должно делать ваше решение. Я работал / играл с Unix в течение длительного времени (с тех пор, как существовал Linux), и, если я чего-то не понимаю, это красный флаг, который, может быть, другие люди тоже не поймут, и это Нужно больше объяснений (ИМНШО). … (Продолжение)
G-Man

(Продолжение)… Так как вам «… нравится думать… что [вы] понимаете почти все, чем обычный человек», возможно, вам следует помнить, что цель Stack Exchange - не быть службой написания команд, тысячи разовых решений тривиально разных вопросов; а научить людей, как решать свои проблемы. И для этого, возможно, вам нужно объяснить все достаточно хорошо, чтобы «средний человек» мог это понять. Посмотрите на ответ Lesmana для примера отличного объяснения. … (Продолжение)
G-Man


7

Приведенное выше решение lesmana также может быть выполнено без дополнительных затрат на запуск вложенных подпроцессов с использованием { .. }взамен (помня, что эта форма сгруппированных команд всегда должна заканчиваться точкой с запятой). Что-то вроде этого:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

Я проверил эту конструкцию с помощью dash версии 0.5.5 и bash версий 3.2.25 и 4.2.42, поэтому, даже если некоторые оболочки не поддерживают { .. }группировку, она по-прежнему совместима с POSIX.


Это работает очень хорошо с большинством оболочек, с которыми я пробовал, включая NetBSD sh, pdksh, mksh, dash, bash. Однако я не могу заставить его работать с AT & T Ksh (93s +, 93u +) или zsh (4.3.9, 5.2), даже с set -o pipefailin ksh или любым количеством набросанных waitкоманд в обоих. Я думаю, что это может быть, по крайней мере частично, проблемой синтаксического анализа для ksh, так как если я придерживаюсь использования подоболочек, то это работает нормально, но даже если ifвыбрать вариант подоболочки для ksh, но оставить составные команды для других, это не удастся ,
Грег А. Вудс

4

Это переносимо, то есть работает с любой POSIX-совместимой оболочкой, не требует записи в текущий каталог и позволяет одновременно запускать несколько сценариев, использующих один и тот же прием.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Изменить: вот более сильная версия после комментариев Жиля:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: и вот немного более легкий вариант после комментария dubiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))

3
Это не работает по нескольким причинам. 1. Временный файл может быть прочитан до его записи. 2. Создание временного файла в общем каталоге с предсказуемым именем небезопасно (тривиальная DoS, гонка символических ссылок). 3. Если один и тот же скрипт использует этот трюк несколько раз, он всегда будет использовать одно и то же имя файла. Чтобы решить 1, прочитайте файл после завершения конвейера. Для решения 2 и 3 используйте временный файл со случайно сгенерированным именем или в личном каталоге.
Жиль

+1 Ну, $ {PIPESTATUS [0]} проще, но основная идея здесь работает, если знать о проблемах, о которых упоминает Жиль.
Йохан

Вы можете сэкономить несколько подоболочек: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Я согласен, что с Bash легче, но в некоторых случаях знание того, как избежать Bash, того стоит.
dubiousjim

4

Следующее подразумевается как дополнение к ответу @Patrik, если вы не можете использовать одно из распространенных решений.

Этот ответ предполагает следующее:

  • У вас есть оболочка, которая не знает $PIPESTATUSниset -o pipefail
  • Вы хотите использовать канал для параллельного выполнения, поэтому нет временных файлов.
  • Вы не хотите иметь дополнительный беспорядок, если прервет сценарий, возможно, из-за внезапного отключения электроэнергии.
  • Это решение должно быть относительно простым для понимания и понятным для чтения.
  • Вы не хотите вводить дополнительные субоболочки.
  • Вы не можете возиться с существующими файловыми дескрипторами, поэтому не следует трогать stdin / out / err (однако вы можете временно ввести новый)

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

  • Все, что вы хотите знать, это то, что все команды в ТРУБЕ имеют код выхода 0.
  • Вам не нужна дополнительная информация о боковой полосе.
  • Ваша оболочка ожидает возвращения всех команд канала.

Before: foo | bar | bazоднако это только возвращает код завершения последней команды ( baz)

Wanted: $?не должно быть 0(true), если какая-либо из команд в конвейере завершилась неудачно

После:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Разъяснение:

  • Tempfile создается с mktemp. Обычно это сразу создает файл в/tmp
  • Этот временный файл затем перенаправляется на FD 9 для записи и FD 8 для чтения
  • Затем временный файл немедленно удаляется. Тем не менее, он остается открытым, пока оба FD не исчезнут.
  • Теперь труба запущена. Каждый шаг добавляет только к FD 9, если произошла ошибка.
  • waitНеобходимо для ksh, потому что kshеще не ждать , пока все трубы команды , чтобы закончить. Однако обратите внимание, что при наличии некоторых фоновых задач возможны нежелательные побочные эффекты, поэтому я закомментировал их по умолчанию. Если ожидание не помешает, вы можете оставить комментарий.
  • После этого содержимое файла читается. Если оно пустое (потому что все работало) readвозвращает false, значит trueуказывает на ошибку

Это может использоваться как замена плагина для одной команды и требует только следующее:

  • Неиспользованные FD 9 и 8
  • Одна переменная окружения для хранения имени временного файла
  • И этот рецепт может быть адаптирован к любой оболочке, которая позволяет перенаправление ввода-вывода
  • Кроме того, он не зависит от платформы и не нуждается в таких вещах, как /proc/fd/N

ошибки:

В этом скрипте есть ошибка на случай /tmpнехватки места. Если вам также нужна защита от этого искусственного случая, вы можете сделать это следующим образом, однако это имеет недостаток, заключающийся в том, что число 0in 000зависит от количества команд в конвейере, поэтому это немного сложнее:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Примечания по переносимости:

  • kshи подобные оболочки, которые ожидают только последнюю команду канала, нуждаются в waitнезакомментированном

  • Последний пример использует printf "%1s" "$?"вместо того, echo -n "$?"потому что это более переносимо. Не каждая платформа интерпретирует -nправильно.

  • printf "$?"будет делать это также, однако printf "%1s"ловит некоторые угловые случаи в случае, если вы запускаете скрипт на какой-то действительно сломанной платформе. (Читайте: если вам случится программировать на paranoia_mode=extreme.)

  • FD 8 и FD 9 могут быть выше на платформах, которые поддерживают несколько цифр. AFAIR POSIX-совместимая оболочка должна поддерживать только однозначные числа.

  • Был протестирован с Debian 8.2 sh, bash, ksh, ash, sashи дажеcsh


3

С некоторой осторожностью это должно сработать:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

Как насчет очистки, как jlliagre? Разве вы не оставляете файл с именем foo-status?
Йохан

@Johan: Если вы предпочитаете мое предложение, не стесняйтесь голосовать за него;) Помимо того, что он не оставляет файл, он имеет преимущество в том, что позволяет нескольким процессам запускать это одновременно, и текущий каталог не должен быть доступен для записи.
Jlliagre

2

Следующий блок if будет выполняться только в случае успешного выполнения команды:

if command; then
   # ...
fi

В частности, вы можете запустить что-то вроде этого:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Который запустит haconf -makerwи сохранит свои stdout и stderr в "$ haconf_out". Если возвращаемое значение from haconfравно true, тогда блок 'if' будет выполнен и grepбудет читать «$ haconf_out», пытаясь сопоставить его с «Cluster уже доступным для записи».

Обратите внимание, что трубы автоматически очищаются; с перенаправлением вы должны быть осторожны, чтобы удалить «$ haconf_out», когда закончите.

Не так элегантно, как pipefail, но законная альтернатива, если эта функциональность недоступна.


1
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$

0

(По крайней мере, с bash) в сочетании с set -eодним может использовать subshell для явной эмуляции pipefail и выхода при ошибке канала

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Так что, если fooпо какой-то причине происходит сбой - остальная часть программы не будет выполнена, и скрипт завершится с соответствующим кодом ошибки. (Это предполагает, что fooпечатает свою ошибку, которой достаточно, чтобы понять причину сбоя)


-1

РЕДАКТИРОВАТЬ : Этот ответ неправильный, но интересный, поэтому я оставлю его для дальнейшего использования.


Добавление !к команде инвертирует код возврата.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #

1
Я думаю, что это не связано. В вашем примере я хочу знать код выхода ls, а не инвертировать код выходаbogus_command
Майкл Мрозек

2
Я предлагаю отозвать этот ответ.
maxschlepzig

3
Ну, похоже, я идиот. Я фактически использовал это в сценарии, прежде чем подумать, что он сделал то, что хотел ОП. Хорошо, что я не использовал это ни для чего важного
Фалмарри
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.