У меня есть два процесса foo
и bar
, связанных с трубой:
$ foo | bar
bar
всегда выходит 0; Я заинтересован в коде выхода foo
. Есть ли способ получить это?
У меня есть два процесса foo
и bar
, связанных с трубой:
$ foo | bar
bar
всегда выходит 0; Я заинтересован в коде выхода foo
. Есть ли способ получить это?
Ответы:
Если вы используете 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
(обратите внимание на отсутствие кавычек!).
pipestatus
в зш. К сожалению, другие оболочки не имеют этой функции.
echo "$pipestatus[1]" "$pipestatus[2]"
.
if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
Есть 3 распространенных способа сделать это:
Первый способ , чтобы установить pipefail
параметр ( ksh
, zsh
или bash
). Это самое простое, и он в основном устанавливает статус $?
выхода для кода завершения последней программы, чтобы он выходил из ненулевого значения (или ноля, если все выходы успешно завершены).
$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1
Bash также имеет переменную-массив $PIPESTATUS
( $pipestatus
in 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
ksh
, но, если взглянуть на справочную страницу , она не поддерживает $PIPESTATUS
или что-то подобное. Это поддерживает pipefail
опцию, хотя.
LOG=$(failed_command | successful_command)
Это решение работает без использования специальных функций 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
Снизу вверх:
#part3
) и right ( #part2
) выполняются. exit $xs
также является последней командой канала, и это означает, что строка из stdin будет статусом выхода всей конструкции.#part2
и, в свою очередь, будет выходным состоянием всей конструкции.#part5
и #part6
) и справа ( filter >&4
) выполняются. Выходные данные filter
перенаправляются в файловый дескриптор 4. В #part1
файловом дескрипторе 4 был перенаправлен на стандартный вывод. Это означает, что выводом filter
является стандартный вывод всей конструкции.#part6
распечатывается в файловый дескриптор 3. В #part3
файловом дескрипторе 3 был перенаправлен на #part2
. Это означает, что статус выхода из #part6
будет окончательным статусом выхода для всей конструкции.someprog
выполнен. Статус выхода принимается #part5
. Stdout берется по трубе #part4
и пересылается filter
. Выход из filter
воли в свою очередь достигает стандартного вывода, как объяснено в#part4
(read; exit $REPLY)
(exec 3>&- 4>&-; someprog)
упрощает до someprog 3>&- 4>&-
.
{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&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
set -o pipefail
внутри скрипта должен быть более устойчивым, например, в случае, если кто-то выполняет скрипт через bash foo.sh
.
-o pipefail
нет в POSIX.
#!/bin/bash -o pipefail
. Ошибка:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
#!
линию за пределами первого, и таким образом это становится /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
Что я делаю, когда это возможно, чтобы передать код выхода из 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
невозможно дифференцироваться, потому что переводы строк приводятся к пробелам.
Поэтому я хотел дать ответ, подобный ответу Лесманы, но я думаю, что мой, пожалуй, немного проще и немного более выгодно решение с чистой оболочкой Борна:
# 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
.
Если у вас установлен пакет moreutils , вы можете использовать утилиту mispipe, которая делает именно то, что вы просили.
Приведенное выше решение lesmana также может быть выполнено без дополнительных затрат на запуск вложенных подпроцессов с использованием { .. }
взамен (помня, что эта форма сгруппированных команд всегда должна заканчиваться точкой с запятой). Что-то вроде этого:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
Я проверил эту конструкцию с помощью dash версии 0.5.5 и bash версий 3.2.25 и 4.2.42, поэтому, даже если некоторые оболочки не поддерживают { .. }
группировку, она по-прежнему совместима с POSIX.
set -o pipefail
in ksh или любым количеством набросанных wait
команд в обоих. Я думаю, что это может быть, по крайней мере частично, проблемой синтаксического анализа для ksh, так как если я придерживаюсь использования подоболочек, то это работает нормально, но даже если if
выбрать вариант подоболочки для ksh, но оставить составные команды для других, это не удастся ,
Это переносимо, то есть работает с любой 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))
(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
. @Johan: Я согласен, что с Bash легче, но в некоторых случаях знание того, как избежать Bash, того стоит.
Следующее подразумевается как дополнение к ответу @Patrik, если вы не можете использовать одно из распространенных решений.
Этот ответ предполагает следующее:
$PIPESTATUS
ниset -o pipefail
Дополнительные предположения. Вы можете избавиться от всего, но этот рецепт слишком сильно забивает, поэтому здесь он не рассматривается:
- Все, что вы хотите знать, это то, что все команды в ТРУБЕ имеют код выхода 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
Разъяснение:
mktemp
. Обычно это сразу создает файл в/tmp
wait
Необходимо для ksh
, потому что ksh
еще не ждать , пока все трубы команды , чтобы закончить. Однако обратите внимание, что при наличии некоторых фоновых задач возможны нежелательные побочные эффекты, поэтому я закомментировал их по умолчанию. Если ожидание не помешает, вы можете оставить комментарий.read
возвращает false
, значит true
указывает на ошибкуЭто может использоваться как замена плагина для одной команды и требует только следующее:
/proc/fd/N
ошибки:
В этом скрипте есть ошибка на случай /tmp
нехватки места. Если вам также нужна защита от этого искусственного случая, вы можете сделать это следующим образом, однако это имеет недостаток, заключающийся в том, что число 0
in 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". Если возвращаемое значение from haconf
равно true, тогда блок '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
$
(По крайней мере, с bash) в сочетании с set -e
одним может использовать subshell для явной эмуляции pipefail и выхода при ошибке канала
set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program
Так что, если foo
по какой-то причине происходит сбой - остальная часть программы не будет выполнена, и скрипт завершится с соответствующим кодом ошибки. (Это предполагает, что foo
печатает свою ошибку, которой достаточно, чтобы понять причину сбоя)
РЕДАКТИРОВАТЬ : Этот ответ неправильный, но интересный, поэтому я оставлю его для дальнейшего использования.
!
к команде инвертирует код возврата.
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.
# =========================================================== #
ls
, а не инвертировать код выходаbogus_command