it-swarm-ru.tech

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

У меня есть два процесса foo и ​​bar, связанные с каналом:

$ foo | bar

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

314
Michael Mrozek

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

277
camh

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

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
253
Patrick

Это решение работает без использования специальных функций 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

exec 3>&- 4>&- перед 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. Канал создан, а команды слева (#part3) и верно (#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
58
lesmana

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

#!/bin/bash -o pipefail

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

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

Правка: пример

[[email protected] ~]# false | true
[[email protected] ~]# echo $?
0
[[email protected] ~]# set -o pipefail
[[email protected] ~]# false | true
[[email protected] ~]# echo $?
1
37
Chris

По возможности я передаю код выхода из 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")
22

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

foo | bar | baz

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

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

17
Jander

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

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

12
mtraceur

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

11
Emanuele Aina

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

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

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

7
pkeller

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

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

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

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

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

Перед: 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

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

  • Tempfile создается с помощью mktemp. Обычно это сразу создает файл в /tmp
  • Этот временный файл затем перенаправляется на FD 9 для записи и FD 8 для чтения
  • Затем временный файл немедленно удаляется. Тем не менее, он остается открытым, пока оба FD не исчезнут.
  • Теперь труба запущена. Каждый шаг добавляет только к 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

5
Tino

Это переносимо, то есть работает с любой 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))
4
jlliagre

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

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

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

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

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

2
Rany Albeg Wein
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
$
1
C.G.

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

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

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

0
noonex