У мене два процеси 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
Ви можете використовувати приклад 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 pipefailksh або будь-якою кількістю окроплених 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. Зазвичай це відразу створює файл у/tmpwaitНеобхідно для 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". Якщо повернене значення з haconftrue відповідає дійсності, тоді блок '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