Вопрос или проблема
У меня есть два процесса foo
и bar
, соединенные через pipe:
$ foo | bar
bar
всегда завершает работу с кодом 0; меня интересует код завершения foo
. Есть ли способ получить его?
bash
и zsh
имеют переменную массива, которая содержит статус выхода каждого элемента (команды) последнего выполненного конвейера оболочки.
Если вы используете 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
(обратите внимание на отсутствие кавычек!).
Существует 3 общих способа сделать это:
Pipefail
Первый способ — установить опцию pipefail
(ksh
, zsh
или bash
). Это самый простой способ, и он устанавливает статус выхода $?
в код выхода последней программы, завершившейся с ошибкой (или 0, если все завершились успешно).
$ 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
Вы можете использовать 3-й пример команды, чтобы получить конкретное значение в конвейере, которое вам нужно.
Отдельные выполнения
Это самый неудобный из решений. Запускайте каждую команду отдельно и фиксируйте статус:
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
Это решение работает без использования специфичных для bash функций или временных файлов. Бонус: в итоге статус выхода действительно является статусом выхода, а не какой-то строкой в файле.
Ситуация:
someprog | filter
вам нужен статус выхода из someprog
и вывод из filter
.
Вот мое решение:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
результатом этой конструкции является stdout из filter
как stdout конструкции и статус выхода из 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>&;amp;1
К сожалению, грамматика bash требует пробелы и точки с запятой для фигурных скобок, поэтому конструкция становится гораздо более обширной.
Для остального этого текста я буду использовать вариант с подпроцессом.
Пример someprog
и filter
:
someprog() {
echo "line1"
echo "line2"
echo "line3"
return 42
}
filter() {
while read line; do
echo "filtered $line"
done
}
((((someprog; echo $? >amp;3) | filter >amp;4) 3>amp;1) | (чтение xs; exit $xs)) 4>amp;1
echo $?
Пример вывода:
отфильтровано line1
отфильтровано line2
отфильтровано line3
42
Примечание: дочерний процесс наследует открытые файловые дескрипторы от родительского. Это означает, что someprog
унаследует открытый файловый дескриптор 3 и 4. Если someprog
записывает в файловый дескриптор 3, это станет статусом выхода. По-настоящему статус выхода будет проигнорирован, так как read
читает только один раз.
Если вы боитесь, что ваш someprog
может записывать в файловый дескриптор 3 или 4, то лучше закрыть файловые дескрипторы перед вызовом someprog
.
(((((exec 3>amp;- 4>amp;-; someprog); echo $? >&3) | filter >amp;4) 3>amp;1) | (чтение xs; exit $xs)) 4>amp;1
Команда exec 3>amp;- 4>amp;-
перед someprog
закрывает файловый дескриптор перед выполнением someprog
, так что для someprog
этих файловых дескрипторов просто не существует.
Это также может быть записано как: someprog 3>amp;- 4>amp;-
Пошаговое объяснение конструкции:
( ( ( ( someprog; #часть6
echo $? >amp;3 #часть5
) | filter >amp;4 #часть4
) 3>amp;1 #часть3
) | (прочитать xs; exit $xs) #часть2
) 4>amp;1 #часть1
Снизу вверх:
- Создается подпроцесс с дескриптором файла 4, перенаправленным в stdout. Это означает, что все, что напечатано в файловый дескриптор 4 в подпроцессе, станет stdout всей конструкции.
- Создается конвейер, и команды слева (
#часть3
) и справа (#часть2
) выполняются.exit $xs
также является последней командой конвейера, и это означает, что строка из stdin станет статусом выхода всей конструкции. - Создается подпроцесс с дескриптором файла 3, перенаправленным в stdout. Это означает, что все, что напечатано в файловый дескриптор 3 в этом подпроцессе, окажется в
#часть2
, и в свою очередь станет статусом выхода всей конструкции. - Создается конвейер, и команды слева (
#часть5
и#часть6
) и справа (filter >amp;4
) выполняются. Выводfilter
перенаправляется на файловый дескриптор 4. В#часть1
файловый дескриптор 4 был перенаправлен на stdout. Это означает, что выводfilter
является stdout всей конструкции. - Статус выхода из
#часть6
выводится на файловый дескриптор 3. В#часть3
файловый дескриптор 3 был перенаправлен в#часть2
. Это означает, что статус выхода из#часть6
станет финальным статусом выхода для всей конструкции. someprog
выполняется. Статус выхода берется в#часть5
. Stdout берется в конвейер в#часть4
и пересылается наfilter
. Вывод изfilter
в свою очередь достигает stdout, как объяснено в#часть4
Хотя это не совсем то, о чем вы спрашивали, вы могли бы использовать
#!/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
Что я делаю, когда возможно, так это передаю код выхода от 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 байт являются атомарными на всех unix-подобных системах, так что части 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, но в наше время доступен на всех серьезных unix-системах.
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 shell и не использующее временные файлы:
exec 4>&ампер;1
error_statuses="`((foo || echo "0:$?" &гт;&три;) |
(бар || эхо "1:$?" &гт;&три) |
(baz || echo "2:$?" &гт;&три)) 3>&амп;1 &амп;4`"
exec 4>&амп;-
$error_statuses
содержит коды статусов всех неудачно завершившихся процессов, в случайном порядке, с индексами, чтобы указать, какая команда вызвала каждый статус.
#если "бар" завершился с ошибкой, вывести его статус:
echo "$error_statuses" | grep '1:' | cut -d: -f2
#проверить, все ли команды завершились успешно:
test -z "$error_statuses"
#проверить, закончилась ли последняя команда успешно:
! echo "$error_statuses" | grep '2:' &амп;/dev/null
Обратите внимание на кавычки вокруг $error_statuses
в моих тестах; без них grep
не сможет различить, так как новые строки будут преобразованы в пробелы.
Итак, я хотел внести ответ, аналогичный решению lesmana, но думаю, что мое, возможно, немного легче и немного более выгодное чисто Bourne-shell решение:
#Вы хотите передать команду1 через команду2:
exec 4>&амп;1
exitstatus=`{ { команда1; печатать $? 1>&амп;3; } | команда2 1>&амп;4; } 3>&амп;1`
#$exitstatus теперь имеет код выхода команды1.
Думаю, это лучше объясняется изнутри наружу — сначала выполняется команда1 и выводит свой регулярный вывод в stdout (файловый дескриптор 1), а затем, когда она завершена, запуск printf выполнится и выводит код выхода команды1 на свой stdout, но этот stdout перенаправлен на файловый дескриптор 3.
Пока выполняется команда1, ее stdout передается в команду2 (вывод printf никогда не попадает в команду2, потому что мы отправляем его на файловый дескриптор 3 вместо 1, который читается конвейером). Затем мы перенаправляем вывод команды2 на файловый дескриптор 4, чтобы он оставался вне файлового дескриптора 1 — потому что мы хотим, чтобы файловый дескриптор 1 был свободен немного позже, потому что мы сбросим вывод printf на файловый дескриптор 3 обратно на файловый дескриптор 1 — потому что это то, что захватывает подстановка команд (обратные ключки), и это то, что будет помещено в переменную.
Заключительный магический трюк состоит в том, что первый exec 4>&амп;1
мы сделали как отдельную команду — он открывает файловый дескриптор 4 как копию внешнего stdout оболочки. Подстановка команд захватывает все, что записано на стандартный выход с точки зрения команд внутри нее — но, поскольку вывод команды2 переходит на файловый дескриптор 4 с точки зрения подстановки команды, подстановка не захватывает его — но как только он “выйдет” из подстановки команды, он фактически все равно поступает на файловый дескриптор 1 всего сценария.
(exec 4>&амп;1
должна быть отдельной командой, потому что многие общие оболочки не любят, когда вы пытаетесь записать в файловый дескриптор внутри подстановки команды, этот открыт в “внешней” команде, использующей подстановку. Так что это самый простой переносимый способ сделать это.)
Вы можете рассмотреть это менее технически и более игровым образом, как если бы выходы команд перепрыгивали друг через друга: команда1 передает в команду2, затем вывод printf перепрыгивает через команду2, чтобы команда2 не поймала его, и затем вывод команды2 перепрыгивает через и выходит из подстановки команды, как только printf вовремя преобразуется, чтобы быть захваченным подстановкой, чтобы это оказалось в переменной, и вывод команды2 устремляется на стандартный выход, как в обычном коду.
Также, как я это понимаю, $?
все еще будет содержать код возврата второй команды в конвейере, потому что задания переменной, подстановки команды и составные команды фактически прозрачны к коду возврата команды внутри них, так что статус возврата команды2 должен распространяться наружу — это и отсутствие необходимости определять дополнительную функцию, это причина, по которой я думаю, что это может быть несколько лучшее решение, чем предложенное lesmana.
С учетом замечаний lesmana, возможно, что команда1 в какой-то момент может использовать файловые дескрипторы 3 или 4, так что для более высокой надежности вы бы сделали:
exec 4>&амп;1
exitstatus=`{ { команда1 3>&амп;-; printf $? 1>&амп;3; } 4>&амп;- | команда2 1>&амп;4; } 3>&амп;1`
exec 4>&амп;-
Заметьте, что я использую составные команды в моем примере, но подпроцессы (с использованием ( )
вместо { }
) также будут работать, хотя могут быть менее эффективны.
Команды наследуют файловые дескрипторы от процесса, который их запускает, так что вся вторая линия унаследует файловый дескриптор 4, и составная команда с 3>&амп;1
унаследует файловый дескриптор 3. Так что 4>&амп;-
гарантирует, что внутренняя составная команда не унаследует файловый дескриптор 4, и 3>&амп;-
не унаследует файловый дескриптор 3, так что команда1 получает более “чистую”, стандартную среду. Вы также можете переместить внутрь 4>&амп;-
рядом с 3>&амп;-
, но я думаю, почему бы просто не ограничить его область настолько, насколько это возможно.
Я не уверен, как часто вещи напрямую используют файловые дескрипторы 3 и 4 — думаю, большинство времени программы используют системные вызовы, которые возвращают не используемые в данный момент файловые дескрипторы, но иногда код записывает на файловый дескриптор 3 напрямую, предполагаю (могу представить программу, проверяющую файловый дескриптор, чтобы узнать, открыт ли он, и использующую его, если он есть, или по-разному ведет себя, если его нет). Так что последнее, вероятно, лучше иметь в виду и использовать для общих случаев.
Если у вас установлен пакет moreutils, вы можете использовать утилиту mispipe, которая делает именно то, о чем вы спрашивали.
Решение lesmana выше также может быть выполнено без нагрузки на запуск вложенных под процессов, используя { .. }
вместо этого (помните, что эта форма сгруппированных команд всегда должна заканчиваться точками с запятой). Что-то вроде этого:
{ { { { someprog; echo $? >&p;3; } | filter >&p;4; } 3>&амп;1; } | stdintoexitstatus; } 4>&амп;1
Я проверил эту конструкцию с dash версии 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)|(бар)); выйти $cat $s;rm $s))
Правка2:
и вот немного легче вариант следуя комментарию dubiousjim:
(s=/tmp/.$$_$RANDOM;{foo;echo $?: >$s;}|bar; exit $(cat $s;rm $s))
Следующее предназначено как дополнение к ответу @Patrik, на случай, если вы не можете использовать одно из общих решений.
Этот ответ предполагает следующее:
- У вас есть оболочка, которая не знает о
$PIPESTATUS
иset -o pipefail
- Вы хотите использовать pipe для параллельного выполнения, так что никаких временных файлов.
- Вы не хотите оставлять лишний мусор, если вы прервете сценарий, возможно, из-за внезапного отключения питания.
- Это решение должно быть относительно понятным и чистым для чтения.
- Вы не хотите вводить дополнительные под оболочки.
- Вы не можете играть с существующими файловыми дескрипторами, так что stdin/out/err не должны касаться (хотя вы можете временно создать новые)
Дополнительные предположения. Вы можете избавиться от всех, но это затрудняет рецепт слишком много, поэтому это здесь не рассматривается:
- Все, что вам нужно знать, это то, что все команды в PIPE имеют статус выхода 0.
- Вам не нужна дополнительная информация по боковой полосе.
- Ваша оболочка ожидает окончания выполнения всех команд pipe.
Ранее: foo | bar | baz
, но это возвращает только код выхода последней команды (baz
)
Желательно: $?
не должен быть 0
(истина), если любая из команд в pipe не удалась.
После:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo || echo $? &амп;9; } |
{ бар || эхо $? &амп;9; } |
{ baz || echo $? &амп;9; }
#ждать
! читать TMPRESULTS &амп;8
} 9>&амп;"$TMPRESULTS" 8<"$TMPRESULTS"
# $? теперь равно 0, только если все команды имели статус выхода 0
Объяснил:
- Создается временный файл с помощью
mktemp
. Это обычно сразу создает файл в/tmp
- Этот временный файл затем перенаправляется на FD 9 для записи и FD 8 для чтения
- Затем временный файл немедленно удаляется. Он остается открытым, пока оба FD не выйдут из существования.
- Теперь запускается pipe. Каждый шаг добавляет к FD 9 только в случае, если была ошибка.
- Команда
wait
необходима дляksh
, потому чтоksh
иначе не ждет окончания всех команд pipe. Однако обратите внимание, что есть нежелательные побочные эффекты, если присутствуют некоторые фоновые задачи, поэтому по умолчанию она закомментирована. Если ожидание не мешает, вы можете раскомментировать. - После этого содержимое файла читается. Если оно пустое (потому что все работало)
read
возвращаетfalse
, так чтоtrue
указывает на ошибку
Это может быть использовано как замена плагина для одной команды и требует следующее:
- Неиспользуемые FD 9 и 8
- Одна переменная среды для хранения имени временного файла
- И этот рецепт может быть адаптирован практически к любой оболочке, поддерживающей IO перенаправление
- Также это довольно платформенно-агностично и не требует таких вещей, как
/proc/fd/N
Ошибки:
Этот скрипт имеет ошибку на случай, если /tmp
закончит место. Если вам нужно защититься от этого искусственного случая тоже, вы можете сделать это следующим образом, однако это имеет недостаток, что количество 0
в 000
зависит от количества команд в pipe, так что это чуть более сложно:
TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"
{ foo; printf "%1s" "$?" &амп;9; } |
{ bar; printf "%1s" "$?" &амп;9; } |
{ baz; printf "%1s" "$?" &амп;9; }
#ждать
чтение TMPRESULTS &pygame;8
[ 000 = "$TMPRESULTS" ]
} 9>&ампер;"$TMPRESULTS" 8<"$TMPRESULTS"
Заметки по портируемости:
-
ksh
и аналогичные оболочки, которые ждут завершения только последней команды pipe, нуждаются в том, чтобыwait
был раскомментирован -
Последний пример использует
printf "%1s" "$?"
вместоecho -n "$?"
, потому что это более переносимо. Не каждая платформа корректно интерпретирует-n
. -
printf "$?"
также сработал бы, однакоprintf "%1s"
улавливает некоторые крайние случаи на случай, если вы запустите скрипт на действительно поломанной платформе. (Читайте: если вы случайно программируете вparanoia_mode=extreme
.) -
FD 8 и FD 9 могут быть выше на платформах, поддерживающих многозначимостью. AFAIK 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 команда; then
# ...
fi
Если конкретно говорить, вы можете запустить что-то вроде этого:
haconf_out=/путь/к/некоторому/временному/файлу
if haconf -makerw > "$haconf_out" 2>&амп;1; then
grep -iq "Кластер уже доступен для записи" "$haconf_out"
# ...
fi
Который выполнит haconf -makerw
и сохранит его stdout и stderr в “$haconf_out”. Если возвращаемое значение из haconf
истина, тогда блок ‘if’ будет выполнен и grep
прочитает “$haconf_out”, пытаясь сопоставить его с “Кластер уже доступен для записи”.
Обратите внимание, что трубы автоматически самоочищаются; с перенаправлением вам нужно будет внимательно следить за тем, чтобы удалить “$haconf_out” после завершения.
Не так элегантно, как pipefail
, но законная альтернатива, если эта функциональность недоступна.
Альтернативный пример для решения @lesmana, возможно упрощенный.
Обеспечивает логирование в файл, если требуется.
=====
$ кошка z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"
exec 8>&амп;- 9>&амп;-
{
{
{
{ #НАЧАЛО - добавьте код ниже этой строки и перед #КОНЕЦ
./zz.sh
echo ${?} 1>&амп;8 # использовать ровно 1 раз перед началом #КОНЕЦ
#КОНЕЦ
} 2>&амп;1 | ${TEE} 1>&амп;9
} 8>&амп;1
} | выйти $(читать; printf "${REPLY}")
} 9>&амп;1
выход ${?}
$ влажный zz.sh
эхо "мой код скрипта..."
выход 42
$ ./z.sh; eco "status=${?}"
мой код скрипта...
статус=42
$
Для всех, кто использует bash
. Думаю, наиболее чистым решением для получения и каждого статуса выхода в pipe является следующее.
-
Один раз подготовьте. Включите
lastpipe
в настройках оболочки. Это позволяет получить значение из последней команды в pipe без использования под облаков. Если вы находитесь в интерактивной оболочке, также отключите управление заданиями:set +m
(последнее не нужно для сценариев – управление заданиями по умолчанию отключено).shopt -s lastpipe set +m
-
прочитать
значение в вашу переменную и используйте PIPESTATUS естественным образом. Например,grep -E "\S" "file.txt" | sort | uniq | читать -d '' RES # 'read' код выхода 1 означает, что весь ввод был прочитан до EOF, мы с этим согласен if (( PIPESTATUS[0] > 1 || PIPESTATUS[1] > 0 || PIPESTATUS[2] > 0 || PIPESTATUS[3] > 1 )); then echo "Ошибка" else echo "$RES" fi
Приведенный выше пример читает все непустые строки из “file.txt”, сортирует их и удаляет дубликаты. В этом примере у нас есть пользовательская обработка кода выхода: код выхода grep
1 означает, что строки не были выбраны, мы с этим согласны; код выхода read
1 ожидается для сохранения многострочного вывода (по крайней мере, я не знаю другого чистого способа сделать это). Чистое означение, что не добавляется код просто для того, чтобы избежать кода выхода 1 у read
.
ОБРАТИТЕ ВНИМАНИЕ: опция read -d ''
используется для чтения всего ввода в нашу переменную (отключая разделитель, на который останавливается read
, который по умолчанию является символом новой строки). Это как мы сохраняем многострочный вывод. Если ваш pipe содержит только одну строку, то вы можете использовать plain read RES
(и, вероятно, можно изменить ожидаемый код выхода с read
на 0, а не на 1, как выше).
ИЗМЕНЕНИЕ: Этот ответ оказался неверным, но интересным, поэтому я оставлю его для дальнейшего использования.
Добавление !
к команде инвертирует возвращаемый код.
http://tldp.org/LDP/abs/html/exit-status.html
# =========================================================== #
# Передача _pipe_ с ! инвертирует возвращаемый статус выхода.
ls | неправильная_команда # bash: неправильная_команда: команда не найдена
echo $? # 127
! ls | неправильная_команда # bash: неправильная_команда: команда не найдена
echo $? # 0
# Обратите внимание, что ! не изменяет выполнение pipe.
# Только статус выхода изменяется.
# =========================================================== #
(По крайней мере, в bash) в сочетании с set -e
можно использовать подпроцесс для явной эмуляции pipefail и выхода в случае ошибки pipe
set -e
foo | bar
(выйти ${PIPESTATUS[0]} )
отдыхая программа
Так, если foo
терпит неудачу по какой-либо причине, оставшаяся часть программы не будет выполняться, и сценарий завершится с соответствующим кодом ошибки.
(Это предполагает, что foo
печатает свою собственную ошибку, чего достаточно, чтобы понять причину сбоя)
Следующий ответ похож на некоторые другие (особенно [0] и [1]) здесь, с отличием:
- Он также захватывает (в переменной) стандартный вывод последней программы в pipe.
Он также:
- пытается быть совместимым с POSIX
- получает коды выхода команд в pipe
- позволяет стандартной ошибке (файловый дескриптор 2) проходить через
Расширения и ограничения:
- Предполагаю, что его можно расширить, чтобы иметь больше одной команды в pipe.
- Но он может вернуть только один из кодов выхода (или объединенный код).
Как вы увидите из моего выбора команд (одна, которая читает двоичные данные и base64
), я использовал это для чтения и хранения двоичных данных (включая 0x0) в переменной Shell (конечно, только в закодированной форме). Если это требует обработки позже, его нужно будет снова декодировать (например, printf '%s' "${stdout_command2}" | base64 -d
| что-то, что может обработать двоичные данные).
Это конструкция:
stdout_command2="$(
exec 4>&амп;1
exitstatus_command1="$(
{
{читать_двоичные 3>&амп;- ; printf $? &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;3 ; } 4>&амп;- | \
base64 -w 0 &амп;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;4 3>&амп;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;-
Ответ или решение
Чтобы получить код завершения процесса, который был перенаправлен в конвейере на другой процесс, вам следует понимать, как в BASH и других оболочках может происходить управление потоками, а также как работают механизмы отслеживания статуса выполнения частей конвейера.
Теория
Когда вы выполняете команды в конвейере, такие как foo | bar
, только окно командной строки возвращает код завершения последнего процесса в этом фоне, то есть bar
. Это ограничение может усложнить получение кода завершения первых частей конвейера, таких как foo
. В традиционной Unix-командной оболочке статус завершения команды будет псевдослучайно браться из последнего завершённого подпроцесса.
Однако в более продвинутых оболочках, таких как bash
и zsh
, существуют специальные массивы – PIPESTATUS
в bash
и pipestatus
в zsh
. Эти массивы содержат коды завершения каждой команды в последнем выполненном конвейере. Это позволяет более точно отслеживать статус завершения всех команд в конвейере.
Пример
Рассмотрим несколько способов, как можно реализовать отслеживание статуса выполнения для foo
, когда он перенаправляется в конвейере на другой процесс.
Использование PIPESTATUS
в Bash
$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
# Результат будет отображать "1 0", вот так мы можем получить код завершения `foo`.
Управление через set -o pipefail
Этот параметр делает так, что конвейер завершает выполнение с кодом последней команды, которая завершилась ошибкой (не нулевым кодом).
$ set -o pipefail
$ false | true
$ echo $?
# Вывод будет "1", так как `false` завершился с ошибкой, и это значение передается всему конвейеру.
Альтернативное использование именованных каналов и управления файлами-дискрипторами
Вы можете использовать именованные каналы (FIFO) и управление открытыми файлами, чтобы независимо отслеживать статус каждой части конвейера.
mkfifo my_fifo
(foo; echo $? > my_fifo) | (bar; cat my_fifo)
# Код завершения foo будет записан в именованный канал и считан отдельно.
Применение
Когда вы пишете скрипты для автоматизации процессов или мониторинга различных этапов выполнения программы в Unix/Linux средах, важно отслеживать не только общий статус выполнения конвейера, но и статус каждой его части. Например, при выполнении сложного обновления базы данных или обработки критично важных данных, ошибка одной из частей может существенно повлиять на результат, и её необходимо обработать до перехода к следующему шагу.
Используя приемы, описанные выше, вы можете построить более надежную систему проверки состояний выполнения для каждой команды в конвейере. Это поможет минимизировать ошибки и восстановить систему после сбоя или избежать их вовсе.
Использование массива PIPESTATUS
или опции pipefail
, а также более сложные конструкции с mkfifo
позволяют построить более изысканные и защищенные от отказов скрипты, что особенно важно для обеспечения корпоративного уровня надежности и управления процессами.
В конечном итоге важно помнить, что ваше решение должно учитывать особенности архитектуры конкретной Unix/Linux системы, на которой будет исполняться ваш скрипт, а также быть достаточно гибким для адаптации к будущим изменениям и улучшениям в вашей инфраструктуре.