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

У меня есть два процесса foo и bar , связанные с трубой:

 $ foo | bar 

bar всегда выходит 0; Меня интересует код выхода foo . Есть ли способ добраться до него?

Если вы используете bash , вы можете использовать PIPESTATUS массива 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 (обратите внимание на отсутствие кавычек!).

Есть три общих способа сделать это:

Pipefail

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

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

$ PIPESTATUS

Bash также имеет переменную массива с именем $PIPESTATUS ( $pipestatus в 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 

В то время как не совсем то, что вы просили, вы можете использовать

 #!/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 

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

Ситуация:

 someprog | filter 

вы хотите получить статус выхода из someprog и выход из filter .

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

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

Пример 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 

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


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

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

Снизу вверх:

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

То, что я делаю, когда это возможно, – это подать код выхода из 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") 

Начиная с трубопровода:

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

Поэтому я хотел внести свой ответ, например, lesmana's, но я считаю, что мой, возможно, немного проще и немного более выгодным чистым решением Bourne-shell:

 # 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 запущен, его stdout передается по каналу в command2 (вывод printf никогда не приводит к команде2, потому что мы отправляем его в дескриптор файла 3 вместо 1, что и читает труба). Затем мы перенаправляем вывод command2 в дескриптор файла 4, так что он также остается вне файлового дескриптора 1 – потому что мы хотим, чтобы дескриптор файла 1 был освобожден немного позже, потому что мы будем выводить вывод printf на дескриптор файла 3 обратно в дескриптор файла 1 – потому что это то, что будет замещать команда (обратные шаги), и это то, что будет помещено в переменную.

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

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

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

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

По обоюдным словам 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>&- не будет наследовать дескриптор файла три, поэтому command1 получает «более чистую», более стандартную среду. Вы также можете переместить внутренний 4>&- рядом с 3>&- , но я думаю, почему бы не ограничить его объем как можно больше.

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

Если у вас установлен пакет moreutils , вы можете использовать утилиту mispipe, которая делает именно то, что вы просили.

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

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

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

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

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

Изменить: вот более сильная версия, следующая за комментариями Gilles:

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

Edit2: и вот немного более легкий вариант, следующий за сомнительным комментарием:

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

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

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

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

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

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

До: foo | bar | baz foo | bar | baz foo | bar | baz , однако это возвращает код выхода последней команды ( baz )

Требуется: $? не должно быть 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 

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

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

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

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

ошибки:

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

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

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

Следующий блок «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». Если возвращаемое значение из haconf истинно, тогда блок «if» будет выполнен, а grep будет читать «$ haconf_out», пытаясь сопоставить его с «Cluster, уже доступным для записи».

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

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

 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 $ 

EDIT : Этот ответ неправильный, но интересный, поэтому я оставлю его для дальнейшего использования.


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

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. # =========================================================== #