Отримайте статус виходу процесу, який передається іншому


Відповіді:


256

Якщо ви використовуєте 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(відзначте відсутність лапок!).


9
І pipestatusв зш. На жаль, інші снаряди не мають цієї функції.
Жиль

8
Примітка: Масиви в zsh починаються контрінтуїтивно з індексу 1, так це echo "$pipestatus[1]" "$pipestatus[2]".
Крістоф Вурм

5
Ви можете перевірити весь конвеєр так:if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
l0b0

7
@JanHudec: Можливо, ви повинні прочитати перші п’ять слів моєї відповіді. Також ласкаво вкажіть, де на запитання було запропоновано відповідь лише POSIX.
camh

12
@JanHudec: Також не було позначено POSIX. Чому ви вважаєте, що відповідь повинна бути POSIX? Це не було зазначено, тому я дав кваліфіковану відповідь. У моїй відповіді немає нічого неправильного, плюс є декілька відповідей на вирішення інших випадків.
camh

237

Існує 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

2
Чорт! Я тільки збирався дописувати про PIPESTATUS.
slm

1
Для довідки є декілька інших методик, обговорених у цьому питанні: stackoverflow.com/questions/1221833/…
slm

1
@Patrick рішення pipestatus працює на баш, просто більше скасування, якщо я використовую ksh скрипт, ви думаєте, що ми можемо знайти щось подібне до pipestatus? , (між тим я бачу, що pipestatus не підтримується ksh)
yael

2
@yael я не використовую ksh, але якщо короткий погляд на його manpage, він не підтримує $PIPESTATUSчи щось подібне. Але він підтримує pipefailваріант.
Патрік

1
Я вирішив піти з pipefail, оскільки це дозволяє мені отримати тут статус невдалої команди:LOG=$(failed_command | successful_command)
vmrob

51

Це рішення працює без використання специфічних функцій 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

Знизу вгору:

  1. Створено нижню частину з дескриптором файлів 4, перенаправленим на stdout. Це означає, що все, що буде надруковано в дескрипторі файлів 4 в підпакеті, закінчиться як stdout всієї конструкції.
  2. Створюється труба і виконуються команди зліва ( #part3) і справа ( #part2). exit $xsтакож є останньою командою труби, і це означає, що рядок з stdin буде статусом виходу всієї конструкції.
  3. Створена нижня оболонка з дескриптором файлів 3, переспрямованим на stdout. Це означає, що все, що буде надруковано в дескрипторі файлів 3 в цій підпакеті, закінчиться, #part2і, в свою чергу, буде статусом виходу всієї конструкції.
  4. Створюється труба і виконуються команди зліва ( #part5і #part6) та праворуч ( filter >&4). Вихід з filterпереспрямовується на дескриптор файлу 4. У #part1дескрипторі файлів 4 було перенаправлено на stdout. Це означає, що вихідний показник filterє складанням всієї конструкції.
  5. Статус виходу з #part6друкується на дескриптор файлу 3. У #part3дескрипторі файлу 3 було переспрямовано на #part2. Це означає, що статус виходу з #part6буде остаточним статусом виходу для всієї конструкції.
  6. someprogвиконується. Статус виходу приймається #part5. Витяжка приймається трубою #part4і направляється в filter. Вихід від filterбуде в свою чергу досягати stdout, як пояснено в#part4

1
Приємно. Щодо функції, яку я міг би просто виконувати(read; exit $REPLY)
1616

5
(exec 3>&- 4>&-; someprog)спрощує до someprog 3>&- 4>&-.
Роджер Пате

Цей метод також працює без { { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
допоміжних оболонок

36

Хоча не зовсім те, про що ви просили, ви могли б використовувати

#!/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

9
set -o pipefailвсередині сценарію має бути більш надійним, наприклад, якщо хтось виконує сценарій через bash foo.sh.
maxschlepzig

Як це працює? у вас є приклад?
Йоган

2
Зауважте, що -o pipefailнемає в POSIX.
scy

2
Це не працює в моєму BASH 3.2.25 (1) -випуску. У верхній частині / tmp / ff у мене є #!/bin/bash -o pipefail. Помилка:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Феліпе Альварес

2
@FelipeAlvarez: Деякі середовища (включаючи Linux) не розбирають пробіли на #!лініях, що виходять за межі першого, і це стає /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
lindes

22

Що я роблю, коли це можливо, - це подати вихідний код 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")

1
Під час використання підходу до тимчасового файлу я вважаю за краще додати пастку для EXIT, яка видаляє всі тимчасові файли, щоб сміття не залишилося, навіть якщо сценарій помирає
miracle173

17

Починаючи з трубопроводу:

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неможливо диференціювати, тому що нові рядки примушуються до пробілів.


12

Тож я хотів надати відповідь на кшталт лесмана, але я вважаю, що моя, можливо, трохи простіша і трохи вигідніша проблема з чистою оболонкою Борна:

# 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.
G-Man

@ G-Man Правильно, я постійно забуваю, що bash не має поняття, що це робиться, коли мова йде про дескриптори файлів, на відміну від оболонок, які я зазвичай використовую (зола, яка постачається з busbox). Я дам вам знати, коли я подумаю про вирішення, яке робить баштину щасливою. Тим часом, якщо у вас зручна коробка для debian, ви можете спробувати її в тирі, або якщо у вас є зручна скринька, ви можете спробувати її з посиланням busbox ash / sh.
mtraceur

@ G-Man Що стосується того, що я очікую, що команда буде робити, і що вона робить в інших оболонках, це перенаправлення stdout з command1, щоб воно не потрапляло під заміну команди, але, як тільки поза межами заміни команди, воно переходить fd3 назад до stdout, так що це, як очікується, командувати2. Коли команда1 закінчується, printf спрацьовує та друкує свій статус виходу, який фіксується у змінну командою підстановки. Дуже детальна розбивка тут: stackoverflow.com/questions/985876/tee-and-exit-status/… Також ваш коментар читався так, ніби він мав на увазі ненавиджу?
mtraceur

З чого почати? (1) Вибачте, якщо ви образилися. "Виглядає цікаво" малося на увазі серйозно; було б чудово, якби щось таке компактне працювало так добре, як ви цього очікували. Крім того, я просто говорив, що не розумію, що має робити ваше рішення. Я працюю / граю з Unix тривалий час (оскільки до існування Linux), і якщо я чогось не розумію, це червоний прапор, який, можливо, і інші люди не зрозуміють, і це потребує більше пояснень (IMNSHO). … (Продовження)
G-Man

(Протяг)… Оскільки ви «… любите думати… що [ви] розумієте майже все, ніж пересічний чоловік», можливо, вам слід пам’ятати, що мета Stack Exchange - не бути сервісом написання команд, а також тисячі одноразових рішень тривіально чітких питань; а навчити людей вирішувати власні проблеми. І для цього, можливо, вам потрібно пояснити речі досить добре, щоб «пересічна людина» могла це зрозуміти. Подивіться на відповідь лесмена для прикладу відмінного пояснення. … (Продовження)
G-Man

11

Якщо у вас встановлений пакет moreutils, ви можете скористатись утилітою неправильного введення , яка виконує саме те, що ви просили.


7

Вирішення лесмана вище може бути виконано і без накладних витрат запуску вкладених підпроцесів, використовуючи { .. }натомість (пам'ятаючи, що ця форма згрупованих команд завжди повинна закінчуватися крапками з комою). Щось на зразок цього:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

Я перевірив цю конструкцію за допомогою тире версії 0.5.5 та bash версій 3.2.25 та 4.2.42, тому навіть якщо деякі оболонки не підтримують { .. }групування, вона все ще сумісна з POSIX.


Це дуже добре працює з більшістю оболонок, з якими я пробував, включаючи NetBSD sh, pdksh, mksh, dash, bash. Однак я не можу змусити його працювати з AT&T Ksh (93s +, 93u +) або zsh (4.3.9, 5.2), навіть з set -o pipefailksh або будь-якою кількістю окроплених waitкоманд у будь-якій. Я думаю, що, частково, це може бути проблема розбору для ksh, так як якщо б я дотримувався використання підзаголовків, то він працює чудово, але навіть якщо ifвибирати варіант для нижньої оболонки для ksh, але залишати складені команди для інших, це не вдається .
Грег А. Вудс

4

Це портативно, тобто працює з будь-якою оболонкою, сумісною з 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))

3
Це не працює з кількох причин. 1. Тимчасовий файл може бути прочитаний перед його написанням. 2. Створення тимчасового файлу у спільному каталозі з передбачуваним іменем небезпечно (тривіальний DoS, змагання з символьною посиланням). 3. Якщо один і той же сценарій кілька разів використовує цей трюк, він завжди використовуватиме одне і те ж ім’я файлу. Щоб вирішити 1, прочитайте файл після завершення конвеєра. Щоб вирішити 2 і 3, використовуйте тимчасовий файл із випадково згенерованим іменем або в приватному каталозі.
Жиль

+1 Ну а $ {PIPESTATUS [0]} простіше, але основна ідея тут працює, якщо хтось знає про проблеми, про які згадує Гілль.
Йоган

Ви можете заощадити кілька подоболочек: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Я погоджуюсь, що з Bash простіше, але в деяких контекстах знати, як уникнути Bash, варто.
сумнівний

4

Далі позначається як доповнення до відповіді @Patrik, якщо ви не в змозі використати одне із поширених рішень.

Ця відповідь передбачає наступне:

  • У вас є оболонка, яка не знає $PIPESTATUSніset -o pipefail
  • Ви хочете використовувати трубу для паралельного виконання, тому немає тимчасових файлів.
  • Ви не хочете мати додаткових затруднень, якщо ви перервете сценарій, можливо, через раптовий відключення живлення.
  • Це рішення повинно бути досить простим у дотриманні та чистим для читання.
  • Ви не хочете вводити додаткові підшивки.
  • Ви не можете поспілкуватися з існуючими дескрипторами файлів, тому stdin / out / err не слід торкатися (однак ви можете ввести якийсь новий тимчасово)

Додаткові припущення. Ви можете позбутися від усіх, але цей клобує рецепт занадто багато, тому він тут не висвітлений:

  • Все, що ви хочете знати, - це те, що всі команди в 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
  • Потім цей темпфайл переспрямовується на FD 9 для запису та FD 8 для читання
  • Потім темпфіл негайно видаляється. Однак він залишається відкритим до тих пір, поки обидва FD не припиняться.
  • Тепер труба запущена. Кожен крок додає лише FD 9, якщо сталася помилка.
  • waitНеобхідно для ksh, тому що kshще не чекати , поки все труби команди , щоб закінчити. Однак зауважте, що в деяких фонових завданнях є небажані побічні ефекти, тому я прокоментував це за замовчуванням. Якщо очікування не зашкодить, ви можете прокоментувати це.
  • Після цього вміст файлу читається. Якщо він порожній (тому що всі працювали) readповертається false, значить, trueвказує на помилку

Це може використовуватися як заміна плагіну для однієї команди і потребує лише наступного:

  • Невикористані FD 9 і 8
  • Єдина змінна середовища, яка містить ім'я тимчасового файлу
  • Цей рецепт може бути адаптований до будь-якої оболонки там, яка дозволяє перенаправляти IO
  • Крім того, він досить агностичний і не потребує таких речей /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


3

Це має бути обережно:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)

Як щодо очищення, як jlliagre? Ви не залишаєте файл, який називається foo-status?
Йоган

@Johan: Якщо ви віддаєте перевагу моїй пропозиції, не соромтесь проголосувати за неї;) Крім того, що не залишати файл, воно має перевагу в тому, що дозволяє декільком процесам запускати це одночасно, і поточний каталог не потребує запису.
jlliagre

2

Наступний блок "якщо" буде запускатися, лише якщо команда "вдалася":

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, але законна альтернатива, якщо ця функціональність недоступна.


1
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
$

0

(З принаймні bash) у поєднанні з set -eодним можна використовувати підзаголовки, щоб явно імітувати pipefail та виходити з помилки труби

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Тож якщо fooз певних причин виходить з ладу - решта програми не буде виконана, а сценарій виходить із відповідним кодом помилки. (Це передбачає, що fooдрукує власну помилку, якої достатньо, щоб зрозуміти причину відмови)


-1

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.
# =========================================================== #

1
Я думаю, що це не пов'язано. У вашому прикладі я хочу знати код виходу ls, а не інвертувати код виходуbogus_command
Michael Mrozek

2
Я пропоную звернути цю відповідь.
maxschlepzig

3
Ну, схоже, я ідіот. Я фактично використовував це в сценарії, перш ніж думав, що він робив те, що хотів ОП. Добре, що я не використав це для нічого важливого
Falmarri
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.