Лови помилок в колонці


97

В даний час у мене є сценарій, який робить щось подібне

./a | ./b | ./c

Я хочу змінити його, щоб у разі виходу будь-якого з a, b або c з кодом помилки я надрукував повідомлення про помилку і зупинився замість того, щоб виводити поганий вихід уперед.

Який був би найпростіший / найчистіший спосіб зробити це?


7
Справді, має бути щось таке, &&|що означало б "продовжувати трубу лише у тому випадку, коли попередня команда була успішною". Я припускаю, що ви також можете мати, |||що означатиме "продовжувати трубу, якщо попередня команда не вдалася" (і, можливо, передавати повідомлення про помилку, як Bash 4 |&).
Призупинено до подальшого повідомлення.

6
@DennisWilliamson, ви не можете «зупинити трубу» , тому що a, b, cкоманди не виконуються послідовно , а паралельно. Іншими словами, потоки даних послідовно від aдо c, але фактично a, bі cкоманди запуску (приблизно) в той же самий час.
Джакомо

Відповіді:


20

Якщо ви дійсно не хочете, щоб друга команда тривала до тих пір, поки не буде відомо, що перша успішна, вам, ймовірно, потрібно використовувати тимчасові файли. Проста версія цього:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Перенаправлення '1> & 2' також може бути скорочено '> & 2'; однак стара версія оболонки MKS неправильно керувала перенаправленням помилок без попереднього "1", тому я використовував це однозначне позначення для надійності для віків.

Це витікає файли, якщо ви щось перериваєте. Програмування оболонок, захищених від бомб (більше чи менше), використовує:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

Перший рядок пастки говорить "запустити команди" rm -f $tmp.[12]; exit 1, коли відбувається будь-який із сигналів 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE або 15 SIGTERM або 0 (коли оболонка виходить з будь-якої причини). Якщо ви пишете сценарій оболонки, для остаточної пастки потрібно лише видалити пастку на 0, яка є пасткою для виходу з оболонки (ви можете залишити інші сигнали на місці, оскільки процес все-таки закінчиться).

У початковому конвеєрі "c" можливо зчитувати дані з "b" до того, як "a" закінчиться - зазвичай це бажано (наприклад, це дає декілька ядер, наприклад). Якщо 'b' є фазою 'сортування', то це не застосовуватиметься - 'b' повинно побачити весь його вхід, перш ніж він зможе генерувати будь-який свій вихід.

Якщо ви хочете виявити, які команди (и) не вдається, ви можете використовувати:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Це просто і симетрично - тривіально поширюватися на 4-частинний або N-частинний трубопровід.

Прості експерименти з «set -e» не допомогли.


1
Я б рекомендував використовувати mktempабо tempfile.
Призупинено до подальшого повідомлення.

@Dennis: так, я думаю, я повинен звикнути до таких команд, як mktemp або tmpfile; вони не існували на рівні оболонки, коли я дізнався це, о так багато років тому. Давайте зробимо швидку перевірку. Я знаходжу mktemp на MacOS X; У мене є mktemp на Solaris, але тільки тому, що я встановив інструменти GNU; видається, що mktemp присутній на антикварному HP-UX. Я не впевнений, чи є загальний виклик mktemp, який працює на платформах. POSIX не стандартизує ні mktemp, ні tmpfile. Я не знайшов tmpfile на платформах, до яких я маю доступ. Отже, я не зможу використовувати команди в переносних скриптах оболонки.
Джонатан Леффлер

1
При використанні trapмайте на увазі, що користувач завжди може надсилати SIGKILLдо процесу негайно його припинення, і в цьому випадку ваша пастка не набере чинності. Те саме стосується, коли у вашої системи відключення живлення. Створюючи тимчасові файли, переконайтеся, що вони використовуються, mktempоскільки вони помістять ваші файли кудись, де вони будуть очищені після перезавантаження (як правило, у /tmp).
Джош

155

У bash ви можете використовувати set -eі set -o pipefailна початку свого файлу. Наступна команда ./a | ./b | ./cзавершиться невдачею, коли будь-який із трьох сценаріїв вийде з ладу. Код повернення буде кодом повернення першого невдалого сценарію.

Зверніть увагу , що pipefailнедоступне в стандартному ш .


Я не знав про трубопровід, дуже зручно.
Філ Джексон

Він призначений для розміщення його в сценарії, а не в інтерактивній оболонці. Вашу поведінку можна спростити до встановлення -e; помилковий; він також закінчує процес оболонки, на який очікується поведінка;)
Мішель Самія

11
Примітка. Це все ще виконає всі три сценарії, а не зупинить трубку при першій помилці.
hyde

1
@ n2liquid-GuilhermeVieira Btw, під "різними варіантами", я спеціально мав на увазі видалити один або обидва set(для 4 різних версій) і подивіться, як це впливає на вихід останньої echo.
Гайд

1
@josch Я знайшов цю сторінку, коли шукав, як це зробити в bash від google, і хоча: "Це саме те, що я шукаю". Я підозрюю, що багато людей, які підтримують відповіді, проходять подібний процес думки і не перевіряють теги та визначення тегів.
Troy Daniels

43

Ви також можете перевірити ${PIPESTATUS[]}масив після повного виконання, наприклад, якщо ви запускаєте:

./a | ./b | ./c

Тоді ${PIPESTATUS}з'явиться масив кодів помилок від кожної команди в трубі, тому якщо середня команда не вдалася, echo ${PIPESTATUS[@]}містила б щось на зразок:

0 1 0

і щось подібне запустіть після команди:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

дозволить вам перевірити, чи всі команди в трубі вдалися.


11
Це bashish --- це розширення bash і не є частиною стандарту Posix, тому інші оболонки, як тире та зола, не підтримують його. Це означає, що ви можете зіткнутися з проблемою, якщо спробувати використати її в скриптах, які починаються #!/bin/sh, тому що якщо shне буде bash, воно не вийде. (Легко піддається фіксації, пам’ятаючи, що використовувати #!/bin/bashнатомість.)
Дано Дано

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- повертає 0, якщо $PIPESTATUS[@]містить лише 0 і пробіли (якщо всі команди в трубі успішні).
MattBianco

@MattBianco Це найкраще рішення для мене. Він також працює з &&, наприклад, command1 && command2 | command3якщо хтось із них виходить з ладу, ваше рішення повертається не нульовим.
DavidC

8

На жаль, для відповіді Джонатана потрібні тимчасові файли, а відповіді Мішеля та Імрона потрібні удари (хоча це питання має мітку оболонки). Як вже вказували інші, не можна перервати трубу до того, як пізніше будуть запущені процеси. Усі процеси запускаються одразу і, таким чином, всі запускаються до того, як будь-які помилки можуть бути передані. Але заголовок питання також запитував про коди помилок. Їх можна отримати та дослідити після закінчення роботи труби, щоб з'ясувати, чи не відбувся жоден із залучених процесів.

Ось рішення, яке фіксує всі помилки в трубі та не лише помилки останнього компонента. Таким чином, це схоже на basha's pipefail, просто більш потужне в тому сенсі, що ви можете отримати всі коди помилок.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Щоб виявити, що щось не вдалося, echoкоманда друкує стандартну помилку, якщо будь-яка команда виходить з ладу. Потім комбінований стандартний вихід помилок зберігається $resі досліджується пізніше. Ось чому стандартна помилка всіх процесів перенаправляється на стандартний вихід. Ви також можете надіслати цей вихід /dev/nullабо залишити його ще одним показником того, що щось пішло не так. Ви можете замінити останній переадресацію на /dev/nullфайл, якщо вам не потрібно зберігати вихід останньої команди де завгодно.

Для того, щоб грати більше з цією конструкцією , і переконати себе , що це дійсно робить те , що він повинен, я замінив ./a, ./bі ./cна подоболочкі, які виконують echo, catі exit. Ви можете використовувати це, щоб перевірити, чи справді ця конструкція пересилає весь результат від одного процесу до іншого і чи правильно записуються коди помилок.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.