Вихід труби та захоплення статусу виходу в Bash


420

Я хочу виконати тривалу команду в Bash, і обидва захоплюють її вихідний статус, і визначають його вихід.

Тому я роблю це:

command | tee out.txt
ST=$?

Проблема полягає в тому, що змінна ST фіксує статус виходу tee а не команди. Як я можу це вирішити?

Зауважте, що команда триває і перенаправлення виводу у файл, щоб переглянути його згодом, не є для мене хорошим рішенням.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Збіг - помилка знайдена" || echo -e "Не співпадає - все добре" Це дозволить перевірити всі значення масиву одразу та подати повідомлення про помилку, якщо будь-яке з повернених значень труби не дорівнює нулю. Це досить надійне узагальнене рішення для виявлення помилок у трубопроводі.
Брайан С. Вілсон

Відповіді:


519

Існує внутрішня змінна Bash $PIPESTATUS; це масив, який містить статус виходу кожної команди у вашому останньому конвеєрі команд переднього плану.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Або іншою альтернативою, яка також працює з іншими оболонками (наприклад, zsh), буде включення pipefail:

set -o pipefail
...

Перший варіант не працює з- zshза трохи іншого синтаксису.


21
Тут є хороше пояснення із прикладами PIPESTATUS AND Pipefail: unix.stackexchange.com/a/73180/7453 .
slm

18
Примітка: $ PIPESTATUS [0] містить статус виходу першої команди в трубі, $ PIPESTATUS [1] статус виходу другої команди тощо.
simpleuser

18
Звичайно, ми маємо пам’ятати, що це специфічно для Баша: якби я (наприклад) написав сценарій для запуску «sh» реалізації BusyBox на моєму пристрої Android або на іншій вбудованій платформі, використовуючи якусь іншу «sh» варіант, це б не працювало.
Асфанд Казі

4
Для тих, хто стурбований розкладом змінної без котирування: Статус виходу у Bash завжди не підписує 8-бітове ціле число , тому цитувати його не потрібно. Це стосується і Unix, як правило, коли статус виходу визначено явно 8-бітним , і передбачається, що він не підписаний навіть самим POSIX, наприклад, при визначенні його логічного заперечення .
Палек

3
Ви також можете використовувати exit ${PIPESTATUS[0]}.
Чаоран

142

використання bash's set -o pipefailкорисно

pipefail: повернене значення конвеєра - це стан останньої команди для виходу з ненульовим статусом, або нульовий, якщо жодна команда не вийшла із ненульовим статусом


23
Якщо ви не хочете змінювати налаштування pipefail для всього сценарію, ви можете встановити параметр лише локально:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan

7
@Jaan Це запустить передплату. Якщо ви хочете цього уникнути, ви можете зробити, set -o pipefailа потім виконати команду, і відразу після цього зробити, set +o pipefailщоб скасувати параметр.
Лінус Арвер

2
Примітка: плакат запитання не хоче "загального коду виходу" труби, він хоче повернути код "команди". З ним -o pipefailвін би знав, якщо труба виходить з ладу, але якщо і команда, і «трій» не вдається, він отримає вихідний код з «трійника».
t0r0X

@LinusArver не очистив би вихідний код, оскільки це успішна команда?
carlin.scott

126

Тупе рішення: з'єднання їх через названу трубу (mkfifo). Тоді команду можна запустити секунду.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

20
Це єдина відповідь на це питання, яка також працює для простої оболонки sh Unix. Дякую!
JamesThomasMoon1979

3
@DaveKennedy: Німий як у "очевидному, не вимагає складних знань синтаксису баш"
EFraim

10
Хоча відповіді на bash є більш витонченими, коли ви маєте перевагу додаткових можливостей bash, це більш кросове рішення платформи. Це також щось над тим, про що варто подумати загалом, оскільки будь-який час, коли ви виконуєте тривалу команду, трубка імен часто є найбільш гнучким способом. Варто зазначити, що деякі системи не мають mkfifoі можуть замість цього вимагати, mknod -pякщо я правильно пам’ятаю.
Харавік

3
Іноді на переповнення стека є відповіді, що ви б стовідсотково стягували гроші, щоб люди перестали робити інші речі, які не мають сенсу, це одна з них. Дякую вам сер.
Dan Chase

1
У випадку, якщо у когось є проблема з mkfifoабо mknod -p: в моєму випадку була належна команда для створення файлу труби mknod FILE_NAME p.
Кароль Гіл

36

Є масив, який дає вам статус виходу кожної команди в трубі.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

26

Це рішення працює без використання специфічних функцій bash або тимчасових файлів. Бонус: врешті-решт статус виходу - це фактично статус виходу, а не якийсь рядок у файлі.

Ситуація:

someprog | filter

ви хочете статус виходу з someprogі вихід зfilter .

Ось моє рішення:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Дивіться мою відповідь на те саме запитання на unix.stackexchange.com, щоб отримати детальне пояснення та альтернативу без додаткових оболонок та деяких застережень.


20

Поєднавши PIPESTATUS[0]та результат виконання exitкоманди в підклітині, ви можете отримати прямий доступ до повернутого значення вашої початкової команди:

command | tee ; ( exit ${PIPESTATUS[0]} )

Ось приклад:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

дасть вам:

return value: 1


4
Дякую, це дозволило мені використовувати конструкцію: VALUE=$(might_fail | piping)яка не встановить PIPESTATUS в головній оболонці, але встановить її рівень помилок. Використовуючи: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})я отримую хочу, що хотів.
vaab

@vaab, цей синтаксис виглядає дуже приємно, але я збентежений тим, що означає "трубопровід" у вашому контексті? Це тільки там, де можна було б виконати «трійку» чи яку б там не було обробку на виході програми може бути_файл? ти!
AnneTheAgile

1
@AnneTheAgile "трубопровід" у моєму прикладі означає команди, з яких ви не хочете бачити errlvl. Наприклад: одна або будь-яка трубопровідна комбінація 'tee', 'grep', 'sed', ... Це не так вже й рідко, що ці команди трубопроводів призначені для форматування або вилучення інформації з більшого виводу або виходу з журналу головного команда: тоді ви більше зацікавлені в errlevel головній команді (тій, яку я назвав 'might_fail' у своєму прикладі), але без моєї конструкції все призначення повертає errlvl останньої трубової команди, яка тут безглузда. Це зрозуміліше?
vaab

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}у випадку не фільтрування
трійки, а грепа

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.

Я думаю, що це найкраще пояснюється зсередини - command1 виконає та надрукує свій звичайний вихід на stdout (файловий дескриптор 1), після чого, виконавши printf, він виконає та надрукує код виходу icommand1 на свій stdout, але цей stdout буде переспрямований на дескриптор файлу 3.

Поки команда1 працює, її stdout передається команді2 (вихід printf ніколи не робить його командою2, тому що ми надсилаємо його дескриптору файлів 3 замість 1, який читає труба). Тоді ми перенаправляємо висновок command2 на дескриптор файлу 4, так що він також залишається поза дескриптором 1 файлу - тому що ми хочемо дескриптор файлу 1 безкоштовно трохи пізніше, тому що ми повернемо результат printf на дескриптор файлу 3 назад вниз в дескриптор файлу 1 - тому що це те, що захоплює підміна команд (backticks), і саме це буде розміщено у змінній.

Останній шматочок магії - це перший exec 4>&1 ми зробили як окрему команду - він відкриває дескриптор файлу 4 як копію викладу зовнішньої оболонки. Заміна команд охоплює все, що написано стандартно, з точки зору команд, що знаходяться всередині неї, але оскільки вихід команд2 збирається в дескриптор 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 безпосередньо, я здогадуюсь (я міг би уявити програму, яка перевіряє дескриптор файлу, щоб побачити, чи він відкритий, і використовувати його, якщо він є, або поводитись відповідно інакше, якщо його немає). Тому останнє, мабуть, найкраще мати на увазі та використовувати для загальних справ.


Приємне пояснення!
selurvedu

6

В Ubuntu та Debian ви можете apt-get install moreutils. Це містить утиліту, яка називається, mispipeяка повертає статус виходу першої команди в трубі.


5
(command | tee out.txt; exit ${PIPESTATUS[0]})

На відміну від відповіді @ cODAR, це повертає вихідний код виходу першої команди, а не лише 0 для успіху та 127 для відмови. Але як зазначив @Chaoran, ви можете просто зателефонувати ${PIPESTATUS[0]}. Однак важливо, щоб все було поставлено в дужки.


4

Поза басом, ви можете:

bash -o pipefail  -c "command1 | tee output"

Це корисно, наприклад, у сценаріях ніндзя, де очікується оболонка /bin/sh.


3

PIPESTATUS [@] потрібно скопіювати у масив одразу після повернення команди pipe. Будь-яке прочитання PIPESTATUS [@] видалить вміст. Скопіюйте його в інший масив, якщо ви плануєте перевірити стан усіх команд труб. "$?" є тим самим значенням, що і останній елемент "$ {PIPESTATUS [@]}", і читання, здається, знищує "$ {PIPESTATUS [@]}", але я не абсолютно це перевірив.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Це не спрацює, якщо труба знаходиться в підкорпусі. Щоб вирішити цю проблему,
див. Bash pipestatus у зворотному режимі?


3

Найпростіший спосіб зробити це в простому ударі - використовувати заміщення процесу замість трубопроводу. Існує кілька відмінностей, але вони, ймовірно, не мають великого значення для вашого випадку використання:

  • Під час запуску конвеєра, bash чекає, поки всі процеси завершаться.
  • Відправлення Ctrl-C в bash змушує знищити всі процеси конвеєра, а не лише основний.
  • pipefailПараметр і PIPESTATUSзмінна НЕ мають ніякого відношення до заміни процесу.
  • Можливо, більше

З заміною процесу, bash просто запускає процес і забуває про нього, його навіть не видно в jobs.

Згадані відмінності вбік consumer < <(producer)і producer | consumerпо суті є рівнозначними.

Якщо ви хочете перевернути, який з них є "головним" процесом, ви просто переверніть команди та напрямок заміни на producer > >(consumer). У вашому випадку:

command > >(tee out.txt)

Приклад:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

Як я вже казав, від виразу труби є відмінності. Процес ніколи не може припинятися, якщо він не чутливий до закриття труби. Зокрема, це може продовжувати писати речі у ваш stdout, що може заплутати.


1

Чистий розчин оболонки:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

А тепер із другим catзамінено на false:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

Зверніть увагу, що перша кішка також виходить з ладу, тому що її стриже закривається. У цьому прикладі порядок відмов команд у журналі є правильним, але не покладайтеся на нього.

Цей метод дозволяє захоплювати stdout та stderr для окремих команд, тож ви можете скинути це також у файл журналу, якщо виникне помилка, або просто видалити його, якщо помилки немає (наприклад, вихід DD).


1

Основа на відповіді @ brian-s-wilson; ця функція помічника в баші:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

використовується таким чином:

1: get_bad_things повинен досягти успіху, але він не повинен давати вихід; але ми хочемо побачити вихід, який він дає

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: всі трубопроводи повинні досягти успіху

thing | something -q | thingy
pipeinfo || return

1

Іноді може бути простіше і зрозуміліше використовувати зовнішню команду, а не заглиблюватися в деталі bash. трубопровід , з мінімального execline мови скриптування процесу , виходить із кодом повернення другої команди *, як це shробить конвеєр, але на відміну від цього sh, він дозволяє змінювати напрямок труби, щоб ми могли захопити код повернення виробника процес (нижче все в shкомандному рядку, але з execlineвстановленим):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

Використання pipelineмає ті ж відмінності від рідних конвеєрів bash, як і заміна процесу bash, що використовується у відповіді # 43972501 .

* Насправді pipelineвзагалі не виходить, якщо не сталася помилка. Він виконується у другу команду, тож повертається друга команда.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.