У мене два процеси 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
Ви можете використовувати приклад 3-ої команди, щоб отримати конкретне значення в конвеєрі, який вам потрібен.
Це найграншіший з рішень. Запустіть кожну команду окремо і зафіксуйте статус
$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
ksh
, але якщо короткий погляд на його manpage, він не підтримує $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
результатом цього конструкта є stdout від filter
як stdout конструкції та статус виходу з someprog
як вихідний статус конструкції.
ця конструкція також працює з простою групуванням команд {...}
замість підпакетів (...)
. передплатники мають певні наслідки, серед інших вартість продуктивності, яка нам тут не потрібна. читайте посібник з тонкого башти для отримання більш детальної інформації: 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 вимагає пробілів і крапки з комою для фігурних дужок, щоб конструкція стала набагато просторішою.
Для решти цього тексту я буду використовувати варіант допоміжної оболонки.
Приклад 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
Знизу вгору:
#part3
) і справа ( #part2
). exit $xs
також є останньою командою труби, і це означає, що рядок з stdin буде статусом виходу всієї конструкції.#part2
і, в свою чергу, буде статусом виходу всієї конструкції.#part5
і #part6
) та праворуч ( filter >&4
). Вихід з filter
переспрямовується на дескриптор файлу 4. У #part1
дескрипторі файлів 4 було перенаправлено на stdout. Це означає, що вихідний показник filter
є складанням всієї конструкції.#part6
друкується на дескриптор файлу 3. У #part3
дескрипторі файлу 3 було переспрямовано на #part2
. Це означає, що статус виходу з #part6
буде остаточним статусом виходу для всієї конструкції.someprog
виконується. Статус виходу приймається #part5
. Витяжка приймається трубою #part4
і направляється в filter
. Вихід від filter
буде в свою чергу досягати stdout, як пояснено в#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.
Я думаю, що це найкраще пояснюється зсередини - команда1 виконає та надрукує свій звичайний вихід на stdout (дескриптор файлу 1), після чого, виконавши printf, він виконає та надрукує код виходу command1 на своєму stdout, але цей stdout буде переспрямований на дескриптор файлу 3.
Поки команда1 працює, її stdout передається команді2 (висновок printf ніколи не робить його командою2, оскільки ми надсилаємо його до дескриптора файлів 3 замість 1, який читає труба). Тоді ми перенаправляємо висновок command2 на дескриптор файлу 4, так що він також залишається поза дескриптором 1 файлу - тому що ми хочемо дескриптор файлу 1 безкоштовно трохи пізніше, тому що ми повернемо результат printf на дескриптор файлу 3 назад вниз в дескриптор файлу 1 - тому що саме те, що захоплює підміна команд (backticks), і саме це буде розміщено у змінній.
Заключний біт магії полягає в тому, що спочатку exec 4>&1
ми зробили як окрему команду - він відкриває дескриптор файлу 4 як копію викладу зовнішньої оболонки. Підстановка команд охоплює все, що написано стандартно, з точки зору команд, що знаходяться всередині неї, але, оскільки висновок command2 збирається в дескриптор 4 файлів, що стосується підстановки команд, підміна команди не захоплює це - однак, Після того, як він "вийде" з підстановки команд, він фактично все ще переходить до загального дескриптора файлу сценарію 1.
(Це exec 4>&1
повинно бути окремою командою, оскільки багатьом звичайним оболонкам це не подобається, коли ви намагаєтесь записати в дескриптор файлу всередині підстановки команди, що відкривається у "зовнішній" команді, яка використовує підстановку. Отже, це найпростіший портативний спосіб це зробити.)
Ви можете дивитись на це менш технічним і грайливим способом, як якщоби результати команд перескакують один одного: command1 передає команду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 безпосередньо, я здогадуюсь (я міг би уявити програму, яка перевіряє дескриптор файлу, щоб побачити, чи він відкритий, і використовувати його, якщо він є, чи поводитись відповідно інакше, якщо його немає). Тому останнє, мабуть, найкраще мати на увазі та використовувати для загальних справ.
-bash: 3: Bad file descriptor
.
Якщо у вас встановлений пакет moreutils, ви можете скористатись утилітою неправильного введення , яка виконує саме те, що ви просили.
Вирішення лесмана вище може бути виконано і без накладних витрат запуску вкладених підпроцесів, використовуючи { .. }
натомість (пам'ятаючи, що ця форма згрупованих команд завжди повинна закінчуватися крапками з комою). Щось на зразок цього:
{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1
Я перевірив цю конструкцію за допомогою тире версії 0.5.5 та bash версій 3.2.25 та 4.2.42, тому навіть якщо деякі оболонки не підтримують { .. }
групування, вона все ще сумісна з POSIX.
set -o pipefail
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: і ось трохи легший варіант після сумнівного коментаря:
(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
Додаткові припущення. Ви можете позбутися від усіх, але цей клобує рецепт занадто багато, тому він тут не висвітлений:
- Все, що ви хочете знати, - це те, що всі команди в PIPE мають код виходу 0.
- Вам не потрібна додаткова інформація про бічні смуги.
- Оболонка чекає повернення всіх команд труби.
Перед: foo | bar | baz
однак це лише повертає код виходу останньої команди ( baz
)
Потрібний: $?
не повинен бути 0
(істинним), якщо будь-яка команда в трубі не виконана
Після:
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
в 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"
Примітки Portablility:
ksh
і подібні оболонки, які чекають лише останньої команди труби, потребують wait
незауваження
Останній приклад використовує printf "%1s" "$?"
замість того, echo -n "$?"
що це більш портативно. Не кожна платформа інтерпретує -n
правильно.
printf "$?"
це також зробить це, однак printf "%1s"
захоплює деякі кутові випадки, якщо ви запускаєте сценарій на якійсь дійсно зламаній платформі. (Читайте: якщо трапляється програмувати paranoia_mode=extreme
.)
FD 8 і FD 9 можуть бути вищими на платформах, які підтримують кілька цифр. ПІСЛЯ ПОСЛІДНОЇ оболонці 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 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
, але законна альтернатива, якщо ця функціональність недоступна.
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
одним можна використовувати підзаголовки, щоб явно імітувати pipefail та виходити з помилки труби
set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program
Тож якщо foo
з певних причин виходить з ладу - решта програми не буде виконана, а сценарій виходить із відповідним кодом помилки. (Це передбачає, що foo
друкує власну помилку, якої достатньо, щоб зрозуміти причину відмови)
EDIT : Ця відповідь неправильна, але цікава, тому я залишу її для подальшого ознайомлення.
!
до команди інвертує код повернення.
http://tldp.org/LDP/abs/html/exit-status.html
# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command # bash: bogus_command: command not found
echo $? # 127
! ls | bogus_command # bash: bogus_command: command not found
echo $? # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
ls
, а не інвертувати код виходуbogus_command