У меня есть два процесса 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( $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
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 pipefailin 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. Обычно это сразу создает файл в/tmpwaitНеобходимо для ksh, потому что kshеще не ждать , пока все трубы команды , чтобы закончить. Однако обратите внимание, что при наличии некоторых фоновых задач возможны нежелательные побочные эффекты, поэтому я закомментировал их по умолчанию. Если ожидание не помешает, вы можете оставить комментарий.readвозвращает false, значит trueуказывает на ошибкуЭто может использоваться как замена плагина для одной команды и требует только следующее:
/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
С некоторой осторожностью это должно сработать:
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