Захопіть stdout і stderr у різні змінні


79

Чи можна зберігати або захоплювати stdout та stderr у різних змінних , не використовуючи тимчасовий файл? Зараз я роблю це, щоб ввести stdout outта stderr під errчас запуску some_command, але я хотів би уникати тимчасового файлу.

error_file=$(mktemp)
out=$(some_command 2>$error_file)
err=$(< $error_file)
rm $error_file

Це питання включає моє запитання, але не відповідає на нього.
ntc2

3
fwiw, це можливо в ksh93. ksh -c 'function f { echo out; echo err >&2; }; x=${ { y=$(f); } 2>&1;}; typeset -p x y'
ormaaj

1
@ormaaj: ти можеш пояснити, як працює техніка ksh93? Можливо, вам доведеться зробити це відповіддю.
Джонатан Леффлер


3
@gniourf_gniourf Ну, це питання є позаду цього, тому, якщо є дублікат, він повинен бути таким: захопіть як stdout, так і stderr в bash :-D

Відповіді:


45

Гаразд, трохи потворно, але ось рішення:

unset t_std t_err
eval "$( (echo std; echo err >&2) \
        2> >(readarray -t t_err; typeset -p t_err) \
         > >(readarray -t t_std; typeset -p t_std) )"

де (echo std; echo err >&2)потрібно замінити фактичною командою. Вихід стандартного виводу зберігається в масиві $t_stdрядок за рядком опускаючи новий рядок ( -t) і STDERR в $t_err.

Якщо вам не подобаються масиви, ви можете це зробити

unset t_std t_err
eval "$( (echo std; echo err >&2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std) )"

що майже імітує поведінку, за var=$(cmd)винятком значення, $?яке веде нас до останньої модифікації:

unset t_std t_err t_ret
eval "$( (echo std; echo err >&2; exit 2 ) \
        2> >(t_err=$(cat); typeset -p t_err) \
         > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"

Тут $?збереглася в$t_ret

Перевірено на Debian хрип за допомогою GNU bash , версія 4.2.37 (1) -випуск (i486-pc-linux-gnu) .


2
Ось чому я б обробляв повернення так само. Спробуйтеeval "$( eval "$@" 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"; exit $t_ret
TheConstructor

1
Дякую за концепцію. Я розширив (дистильована) це трохи тут: stackoverflow.com/a/28796214/2350426

1
typeset -p t_outі typeset -p t_errможе бути змішаним, що робить вихід марним.
4ae1e1

1
@ 4ae1e1 Я думав про таку можливість, але не зміг підтвердити, що це може статися.
TheConstructor

1
@TheConstructor Хм, я думаю, ти маєш рацію. Я був у Zsh і використовував >>()замість > >(). Перший - заборона в Баші; у Zsh він правильно аналізує частину заміщення процесу, але іноді видає спотворений результат. Не знаю, чому, але, > >()здається, працює надійно. Я все ще не повністю переконаний. typeset -pточно не атомна, правда?
4ae1e1

21

Це для лову stdout і stderr у різних змінних. Якщо ви хочете лише зловити stderr, залишивши stdoutяк є, є краще і коротше рішення .

До підвести все вгору для зручності читача, ось

Легке багаторазове bashрішення

Ця версія дійсно використовує під оболонки і працює без tempfiles. (Для tempfileверсії, яка працює без підшелупок, див. Іншу мою відповідь .)

: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
  { __1="$("${@:3}")"; } 2>&1;
  ret=$?;
  printf '%q=%q\n' "$1" "$__1" >&2;
  exit $ret
  )"
ret="$?";
printf '%s=%q\n' "$2" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}

Приклад використання:

dummy()
{
echo "$3" >&2
echo "$2" >&1
return "$1"
}

catch stdout stderr dummy 3 $'\ndiffcult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n'

printf 'ret=%q\n' "$?"
printf 'stdout=%q\n' "$stdout"
printf 'stderr=%q\n' "$stderr"

це друкує

ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n  data  '

Тож його можна використовувати, не задумуючись над цим. Просто покладітьcatch VAR1 VAR2 перед будь-яким command args..і все готово.

Дещо if cmd args..; then стануть if catch VAR1 VAR2 cmd args..; then. Дійсно нічого складного.

Обговорення

З: Як це працює?

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

catch()в основному використовує evalдля встановлення двох змінних. Це схоже на https://stackoverflow.com/a/18086548

Розглянемо дзвінок catch out err dummy 1 2a 3b:

  • давайте поки що пропустимо eval "$({і __2="$(. Я до цього піду пізніше.

  • __1="$("$("${@:3}")"; } 2>&1;виконує dummy 1 2a 3bі зберігає його stdoutв __1для подальшого використання. Так __1стає 2a. Він також перенаправляє stderrз dummyдо stdout, таким чином, що зовнішній улов може зібратиstdout

  • ret=$?; ловить код виходу, який є 1

  • printf '%q=%q\n' "$1" "$__1" >&2;потім виводить out=2aна stderr. stderrтут використовується, так як струм stdoutвже взяв на себе роль stderrв dummyкоманді.

  • exit $retпотім перенаправляє код виходу ( 1) на наступний етап.

Тепер до зовнішнього __2="$( ... )":

  • Це улови stdoutз перерахованих вище, який є stderrпо dummyвиклику, в змінний __2. (Ми могли б використати повторно__1 тут , але раніше я __2робив це менш заплутаним.). Так __2стає3b

  • ret="$?";знову ловить (повернутий) код повернення 1(від dummy)

  • printf '%s=%q\n' "$2" "$__2" >&2;потім виводить err=3aна stderr. stderrвикористовується знову, як це вже було використано для виведення іншої змінної out=2a.

  • printf '( exit %q )' "$ret" >&2;потім виводить код для встановлення належного значення повернення. Я не знайшов кращого способу, оскільки для присвоєння його змінній потрібне ім'я змінної, яке потім не може бути використано як перший або другий аргумент дляcatch .

Зверніть увагу, що для оптимізації ми могли б записати ці 2 printfяк єдиний, наприкладprintf '%s=%q\n( exit %q ) "$ __ 2" "$ ret" `.

То що ми маємо досі?

Ми написали до stderr:

out=2a
err=3b
( exit 1 )

де outвід $1, 2aвід stdoutз dummy, errвід $2, 3bвід stderrз dummyі1 від коду повернення з dummy.

Зверніть увагу, що %qу форматі printfпіклується про цитування таким чином, щоб оболонка бачила належні (одиничні) аргументи, коли мова йде про eval. 2aі3b настільки прості, що копіюються буквально.

Тепер до зовнішнього eval "$({ ... } 2>&1 )";:

Це виконує все вищезазначене, що виводить 2 змінні та exit, ловить їх (для цього 2>&1) і аналізує в поточну оболонку, використовуючиeval .

Таким чином встановлюються 2 змінні та код повернення.

З: Він використовує evalзло. То чи безпечно?

  • Поки printf %qнемає помилок, це повинно бути безпечним. Але завжди потрібно бути дуже обережним, просто подумайте про ShellShock.

З: Помилки?

  • Очевидних помилок не відомо, окрім наступних:

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

    • Як зазвичай, $(echo $'\n\n\n\n') ковтає всі лінії подачі , не тільки останню. Це вимога POSIX. Якщо вам потрібно отримати LF неушкодженими, просто додайте до виводу якийсь завершальний символ і видаліть його згодом, як у наступному рецепті (подивіться кінцевий результат, xякий дозволяє прочитати програмне посилання, що вказує на файл, який закінчується на a $'\n'):

          target="$(readlink -e "$file")x"
          target="${target%x}"
      
    • Змінні-оболонки не можуть нести байт NUL ( $'\0'). Вони просто ігноруються, якщо трапляються у stdoutабо stderr.

  • Дана команда виконується у допоміжній оболонці. Тому він не має доступу до $PPIDзмінних оболонки і не може його змінювати. Ви можете catchфункцію оболонки, навіть вбудовані, але вони не зможуть змінювати змінні оболонки (оскільки все, що працює всередині, $( .. )не може цього зробити). Отже, якщо вам потрібно запустити функцію в поточній оболонці і зловити її stderr / stdout, вам потрібно зробити це звичайним способом за допомогою tempfiles. (Є способи зробити це таким чином, що переривання оболонки, як правило, не залишає сміття позаду, але це складно і заслуговує на свою власну відповідь.)

Питання: версія Bash?

  • Я думаю, вам потрібен Bash 4 і вище (завдяки printf %q)

З: Це все ще виглядає настільки незручно.

  • Правильно. Інша відповідь тут показує, як це можна зробити kshнабагато чіткіше. Однак я не звик ksh, тому залишаю за собою іншим створити подібний простий для повторного використання рецепт ksh.

З: Чому б kshтоді не використовувати ?

  • Тому що це bashрішення

З: Сценарій можна вдосконалити

  • Звичайно, ви можете вичавити кілька байтів і створити менші або незрозуміліші рішення. Просто піти на це;)

З: Існує помилка. : catch STDOUT STDERR cmd args..прочитати# catch STDOUT STDERR cmd args..

  • Власне це і призначено. :з’являється, bash -xпоки коментарі мовчки ковтаються. Отже, ви можете побачити, де знаходиться парсер, якщо трапляється, що у визначенні функції є помилка. Це старий фокус налагодження. Але будьте обережні, ви можете легко створити якісні побічні ефекти в рамках аргументів :.

Редагувати: Додано ще пару, ;щоб полегшити створення однокласника з catch(). І додав розділ, як це працює.


Це дуже цікаве рішення, враховуючи, що це значно полегшує використання. Однак слід надати трохи більше деталей про те, як це працює, враховуючи, що воно не відповідає загальній схемі інших запропонованих рішень.
jwatkins

А як щодо цього catchдля команд, які переспрямовують один із потоків або є трубопроводами? Може здатися сумнівним спробувати захопити обидва виходи - один з них порожній (оскільки сама команда все одно перенаправляє його). Але це полегшує використання одного і того ж шаблону знову і знову з будь-якою командою (особливо якщо команда надається зовні, і ви не знаєте, чи перенаправляє), навіть якщо в деяких випадках одна зі змінних приречена бути порожньою.
Адам Бадура

Поки що я знайшов просте обхідне рішення. Просто визначте просту функцію типу, function echo_to_file { echo -n "$1" >"$2" ; }а потім використовуйте catchз цією функцією. Працює, як очікувалося. Але все-таки було б непогано мати його в catchсобі. (Подібний "фокус" можна зробити, щоб в команді були труби.)
Адам Бадура,

@AdamBadura питання полягало в тому, щоб паралельно ловити 2 різні змінні. Якщо ви хочете захопити лише одну змінну, вам тут це не потрібно catch! Мінлива з вилов вбудована в корпус: стандартний висновок + STDERR: var="$(command 2>&1)"; echo "command gives $? and outputs '$var'"; ловити stderr і перенаправляти stdout: var="$(command 2>&1 >FILE)"(ні >FILE 2>&1, це перенаправляє stderr на FILE!); stdout-only:, var="$(command)"; echo "command gives $? and has stdout '$var'"а для stderrабо інших ФО див іншу відповідь
Тіно

У функції catch, чи не повинен бути остаточний оператор printf printf 'return %q\n' "$ret" >&2? Хочеться, щоб функція catchповертала cmdкод виходу, а не вихід із програми.
Jim Fischer

15

Ця команда встановлює значення stdout (stdval) і stderr (errval) у поточній запущеній оболонці:

eval "$( execcommand 2> >(setval errval) > >(setval stdval); )"

за умови, що ця функція була визначена:

function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

Змініть команду execcommand на захоплену команду, будь то "ls", "cp", "df" тощо.


Все це базується на ідеї, що ми могли б перетворити всі захоплені значення в текстовий рядок за допомогою функції setval, тоді setval використовується для захоплення кожного значення в цій структурі:

execcommand 2> CaptureErr > CaptureOut

Перетворити кожне значення захоплення на виклик setval:

execcommand 2> >(setval errval) > >(setval stdval)

Оберніть все усередині виконуваного виклику та повторіть його:

echo "$( execcommand 2> >(setval errval) > >(setval stdval) )"

Ви отримаєте оголошення, які створює кожен setval:

declare -- stdval="I'm std"
declare -- errval="I'm err"

Щоб виконати цей код (і отримати набір vars), використовуйте eval:

eval "$( execcommand 2> >(setval errval) > >(setval stdval) )"

і, нарешті, перегукуються з набором vars:

echo "std out is : |$stdval| std err is : |$errval|

Також можна включити значення повернення (виходу).
Повний приклад сценарію bash виглядає так:

#!/bin/bash --

# The only function to declare:
function setval { printf -v "$1" "%s" "$(cat)"; declare -p "$1"; }

# a dummy function with some example values:
function dummy { echo "I'm std"; echo "I'm err" >&2; return 34; }

# Running a command to capture all values
#      change execcommand to dummy or any other command to test.
eval "$( dummy 2> >(setval errval) > >(setval stdval); <<<"$?" setval retval; )"

echo "std out is : |$stdval| std err is : |$errval| return val is : |$retval|"

2
Існує умова гонки, оскільки declareжодна атомна запис не виконує, коли весь вивід перевищує 1008 байт (Ubuntu 16.04, bash 4.3.46 (1)). Існує неявна синхронізація між двома setvalвикликами stdout та stderr ( catin setvalfor stderr не може закінчити до того, як setvalfor stdout закриє stderr). Однак немає синхронізації setval retval, отже, вона може виникати де завгодно. У цьому випадку retvalпроковтується одна з двох інших змінних. Тож retvalсправа не працює надійно.
Тіно

Думаю, мені подобається такий підхід .. певно. Чи є спосіб перенести цей eval в окрему функцію і передати йому команду? Коли я спробую це, воно не оголошує errval або stdval.
Джастін

Я зробив capturable(){...}(setval, як написано) і capture(){ eval "$( $@ 2> >(capturable stderr) > >(capturable stdout); )"; test -z "$stderr" }. capture make ... && echo "$stdout" || less <<<"$stderr"сторінки stderr або друкує stdout, якщо такого немає. Це працює для вас чи допомагає вам, якщо так?
Джон П

14

Джонатан має відповідь . Для довідки, це фокус ksh93. (потрібна нестародавня версія).

function out {
    echo stdout
    echo stderr >&2
}

x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values

виробляє

x=stderr
y=stdout

${ cmds;}Синтаксис просто підстановки команд , яка не створює подоболочкі. Команди виконуються в поточному середовищі оболонки. Простір на початку важливий ({ це зарезервоване слово).

Stderr внутрішньої групи команд перенаправляється на stdout (так що це стосується внутрішньої заміни). Далі, stdout з outприсвоюється y, і перенаправлений stderr захоплюєтьсяx , без звичайних втратy для підкоманди підстановки команди.

Це неможливо в інших оболонках, оскільки всі конструкції, які фіксують вихідні дані, вимагають поміщення виробника в підшорстку, яка в цьому випадку включатиме призначення.

оновлення: тепер також підтримується mksh.


2
Дякую. Ключовим моментом є те, що ${ ... }це не під-оболонка, що робить решту легко поясними. Акуратний трюк, якщо у вас є kshдля використання.
Джонатан Леффлер

10
Це не відповідь на запитання. Питання стосується bash, тоді як ваша відповідь дійсна на ksh.
mshamma

1
@mshamma Очевидно. Прочитайте останній абзац.
ormaaj

14

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

#!/bin/bash -e

foo () {
    echo stdout1
    echo stderr1 >&2
    sleep 1
    echo stdout2
    echo stderr2 >&2
}

rm -f stdout stderr
mkfifo stdout stderr
foo >stdout 2>stderr &             # blocks until reader is connected
exec {fdout}<stdout {fderr}<stderr # unblocks `foo &`
rm stdout stderr                   # filesystem objects are no longer needed

stdout=$(cat <&$fdout)
stderr=$(cat <&$fderr)

echo $stdout
echo $stderr

exec {fdout}<&- {fderr}<&- # free file descriptors, optional

Ви можете мати кілька фонових процесів таким чином і асинхронно збирати їх stdouts та stderrs у зручний час тощо.

Якщо вам це потрібно лише для одного процесу, ви можете так само використовувати твердо закодовані числа fd, такі як 3 і 4, замість {fdout}/{fderr}синтаксису (який знаходить вам безкоштовний fd).


11

Думаю, перед тим, як сказати «ти не можеш» щось зробити, людям слід хоча б спробувати своїми руками ...

Просте і чисте рішення, без використання evalчи чогось екзотичного

1. Мінімальна версія

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(some_command)" 1>&2) 2>&1)

Потрібно: printf ,read

2. Простий тест

Фіктивний сценарій для створення stdoutта stderr:useless.sh

#!/bin/bash
#
# useless.sh
#

echo "This is stderr" 1>&2
echo "This is stdout" 

Фактичний скрипт, який буде фіксувати stdoutта stderr:capture.sh

#!/bin/bash
#
# capture.sh
#

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\0' "$(./useless.sh)" 1>&2) 2>&1)

echo 'Here is the captured stdout:'
echo "${CAPTURED_STDOUT}"
echo

echo 'And here is the captured stderr:'
echo "${CAPTURED_STDERR}"
echo

Вихідні дані capture.sh

Here is the captured stdout:
This is stdout

And here is the captured stderr:
This is stderr

3. Як це працює

Команда

(printf '\0%s\0' "$(some_command)" 1>&2) 2>&1

надсилає стандартний вивід some_commandto printf '\0%s\0', створюючи таким чином рядок \0${stdout}\n\0(де \0є NULбайт і \nє символом нового рядка); рядок\0${stdout}\n\0Потім перенаправляється на стандартну помилку, де some_commandвже була присутня стандартна помилка , таким чином складаючи рядок ${stderr}\n\0${stdout}\n\0, який потім перенаправляється назад до стандартного виводу.

Згодом команда

IFS=$'\n' read -r -d '' CAPTURED_STDERR;

починає читати рядок ${stderr}\n\0${stdout}\n\0до першого NULбайта і зберігає вміст у файлі${CAPTURED_STDERR} . Потім команда

IFS=$'\n' read -r -d '' CAPTURED_STDOUT;

продовжує читати той самий рядок до наступного NUL байта і зберігає вміст у файлі${CAPTURED_STDOUT} .

4. Робить його незламним

Наведене вище рішення покладається на NULбайт для роздільника між stderrі stdout, тому воно не буде працювати, якщо з якоїсь причини stderrмістить іншеNUL байти.

Хоча цього ніколи не повинно статися, можна зробити скрипт абсолютно незламним, видаливши всі можливі NULбайти з stdoutі stderrперед передачею обох виходів до read(санітарії) - NULбайти все одно загубляться, оскільки неможливо зберегти їх у змінних оболонки :

{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
} < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1)

Потрібно: printf , read,tr

РЕДАГУВАТИ

Я видалив ще один приклад поширення статусу виходу на поточну оболонку, оскільки, як зазначав Енді в коментарях, він не був настільки “незламним”, як передбачалося (оскільки він не використовував printfдля буферизації одного з потоки). Для запису я вставляю сюди проблемний код:

Збереження статусу виходу (як і раніше незламний)

Наступний варіант також поширює статус виходу з some_commandпоточної оболонки:

{
  IFS= read -r -d '' CAPTURED_STDOUT;
  IFS= read -r -d '' CAPTURED_STDERR;
  (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr -d '\0'; printf '\0'; } 2>&1- 1>&4- | tr -d '\0' 1>&4-) 3>&1- | xargs printf '\0%s\0' 1>&4-) 4>&1-)

Потрібно: printf , read, tr,xargs

Потім Енді подав наступне «запропоноване редагування» для отримання вихідного коду:

Просте та чисте рішення, що зберігає значення виходу

Ми можемо додати до кінця stderr, третю інформацію, ще одну NULплюс exitстатус команди. Він буде виведений після, stderrале ранішеstdout

{
  IFS= read -r -d '' CAPTURED_STDERR;
  IFS= read -r -d '' CAPTURED_EXIT;
  IFS= read -r -d '' CAPTURED_STDOUT;
} < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1)

Здається, його рішення працює, але має незначну проблему, що статус виходу слід розміщувати як останній фрагмент рядка, щоб ми могли запускати exit "${CAPTURED_EXIT}"в круглих дужках і не забруднювати глобальний обсяг, як я намагався зробити в видалений приклад. Інша проблема в тому , що, так як вихід з його таємних printfотримує відразу додається до stderrпро some_command, ми можемо більше не дезінфікувати можливі NULбайти stderr, тому що серед них тепер є також Наші NUL роздільник.

Трохи задумавшись над кінцевим підходом, я запропонував рішення, яке використовує printfдля кешування обох stdout та код виходу як два різні аргументи, щоб вони ніколи не заважали.

Перше, що я зробив, це намітив спосіб повідомлення статусу виходу до третього аргументу printf , і це було щось дуже легко зробити у найпростішій формі (тобто без санітарії).

5. Збереження статусу виїзду - проект (без санітарної обробки)

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(some_command)" "${?}" 1>&2) 2>&1)

Потрібно: exit , printf,read

Однак, коли ми намагаємось запровадити санітарну обробку, все стає дуже брудно. Запуск trдля дезінфекції потоків насправді перезаписує наш попередній статус виходу, тому, очевидно, єдиним рішенням є перенаправлення останнього в окремий дескриптор, перш ніж він загубиться, зберігати його там, покиtr не зробить свою роботу двічі, а потім перенаправити назад на місце .

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

6. Збереження статусу виходу з санітарною обробкою - незламний (переписаний)

Код нижче - це перепис прикладу, який я видалив. Він також дезінфікує можливі NULбайти в потоках, так що readзавжди може працювати належним чином.

{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '\0%s\0%d\0' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)

Потрібно: exit , printf,read,tr

Це рішення дійсно надійне. Код виходу завжди зберігається відокремленим в іншому дескрипторі, поки не дійдеprintf безпосередньо як окремий аргумент.

7. Кінцеве рішення - функція загального призначення зі статусом виходу

Ми також можемо перетворити наведений вище код у загальну функцію.

# SYNTAX:
#   catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch() {
    {
        IFS=$'\n' read -r -d '' "${1}";
        IFS=$'\n' read -r -d '' "${2}";
        (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
    } < <((printf '\0%s\0%d\0' "$(((({ ${3}; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
}

Потрібно: cat , exit, printf, read,tr

За допомогою catchфункції ми можемо запустити такий фрагмент,

catch MY_STDOUT MY_STDERR './useless.sh'

echo "The \`./useless.sh\` program exited with code ${?}"
echo

echo 'Here is the captured stdout:'
echo "${MY_STDOUT}"
echo

echo 'And here is the captured stderr:'
echo "${MY_STDERR}"
echo

і отримати такий результат:

The `./useless.sh` program exited with code 0

Here is the captured stdout:
This is stderr 1
This is stderr 2

And here is the captured stderr:
This is stdout 1
This is stdout 2

8. Що відбувається в останніх прикладах

Тут слід швидка схематизація:

  1. some_commandзапускається: ми тоді маємо some_command«S stdoutна дескриптор 1, some_command" S stderrна дескриптор 2 і some_command«s код виходу перенаправлені на дескриптор 3
  2. stdoutпідводиться до tr(санітарія)
  3. stderrзамінюється на stdout(тимчасово використовуючи дескриптор 4) і передається доtr (санітарія)
  4. код виходу (дескриптор 3) міняється місцями stderr(тепер дескриптор 1) і передається доexit $(cat)
  5. stderr (тепер дескриптор 3) перенаправляється на дескриптор 1, кінець розширений як другий аргумент printf
  6. код виходу exit $(cat)фіксується третім аргументомprintf
  7. вихідні дані printfпереспрямовуються до дескриптора 2, де stdoutвін уже був присутній
  8. конкатенація stdoutта вихідні дані printfтрубопроводуread

9. Версія № 1 (сумісна з POSIX)

Заміни процесів ( < <()синтаксис) не є стандартом POSIX (хоча вони де-факто є). У оболонці, яка не підтримує < <()синтаксис, єдиним способом досягти того самого результату є <<EOF … EOFсинтаксис. На жаль, це не дозволяє нам використовувати NULбайти як роздільники, оскільки вони автоматично видаляються перед досягненням read. Ми повинні використовувати інший роздільник. Природний вибір падає на CTRL+Zперсонажа (символ ASCII № 26). Ось нестійка версія (вихідні дані ніколи не повинні містити CTRL+Zсимвол, інакше вони змішаються).

_CTRL_Z_=$'\cZ'

{
    IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDERR;
    IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" CAPTURED_STDOUT;
    (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; exit ${_ERRNO_});
} <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(some_command)" "${?}" 1>&2) 2>&1)
EOF

Потрібно: exit , printf,read

10. Версія №2, сумісна з POSIX (непорушна, але не така добра, як версія, яка не є POSIX)

І ось його незламна версія, безпосередньо у формі функції (якщо один stdoutабо stderrмістить CTRL+Zсимволи, потік буде усічений, але ніколи не буде обмінюватися іншим дескриптором).

_CTRL_Z_=$'\cZ'

# SYNTAX:
#     catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch_posix() {
    {
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${1}";
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "${2}";
        (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_});
    } <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ${3}; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
EOF
}

Потрібно: cat , cut, exit, printf, read,tr


Це дуже розумно.
Енді

Остання версія працює некоректно. Спробуйте команду find /procяк некореневу. Попередні версії працюють чудово, оскільки ви використовуєте аргумент printf для "буферизації" stdout, гарантуючи, що stdout не буде надруковано до завершення команди, а 100% stderr буде потоково передано та очищено. Однак остання версія не використовує printf для обробки одного з потоків, а лише код виходу. Stderr і stdout перемежовуються, і stderr містить лише одну вартість. Якщо ви виправите це, пояснення буде дуже вдячне, оскільки я гублюся після введення FD 4
Енді

Привіт Енді! Дякуємо за ваші коментарі та пропозиції щодо редагування. Який результат виходить capture.shна вашій машині після виправлення її з третьою версією?
madmurphy

Гаразд, я бачу це зараз. Остання версія усікається stderr, чи не так?
madmurphy

1
Влучне зауваження. Ваше рішення працює, але має незначну проблему, що статус виходу повинен представляти останній шматок рядка, якщо ми хочемо мати можливість робити exit "${CAPTURED_EXIT}"в круглих дужках не забруднюючи глобальну область, як я намагався зробити у своєму останньому прикладі . Інша проблема полягає в тому , що, так як вихід з ваших сокровенних printfотримує негайно додається до stderrпро some_command, ми можемо більше не дезінфікувати можливі NULбайти stderr, так як серед них є і наш NUL роздільник. Я буду думати про щось найближчими днями.
madmurphy

4

На користь читача ось рішення за допомогою tempfiles.

Питання полягало не в тому, щоб використовувати tempfiles. Однак це може бути пов'язано з небажаним забрудненням /tmp/tempfile на випадок загибелі оболонки. У випадку з kill -9деякими trap 'rm "$tmpfile1" "$tmpfile2"' 0не стріляє.

Якщо ви потрапили в ситуацію, коли можете використовуватиtempfile , але ніколи не хочете залишати за собою сміття , ось рецепт.

Знову він викликається catch()(як і моя інша відповідь ) і має той самий синтаксис виклику:

catch stdout stderr command args..

# Wrappers to avoid polluting the current shell's environment with variables

: catch_read returncode FD variable
catch_read()
{
eval "$3=\"\`cat <&$2\`\"";
# You can use read instead to skip some fork()s.
# However read stops at the first NUL byte,
# also does no \n removal and needs bash 3 or above:
#IFS='' read -ru$2 -d '' "$3";
return $1;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "$1";
"${@:3}" 66<&-;
catch_read $? 66 "$2";
} 2>&1 >"$1" 66<"$1";
}

: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}

Що це робить:

  • Це створює два tempfiles для stdoutі stderr. Однак він майже відразу видаляє їх, так що вони існують лише дуже короткий час.

  • catch_1()ловить stdout(FD 1) у змінну і рухається stderrдо stdoutтакої, що наступний («ліворуч») catch_1може це вловити.

  • Обробка catchвиконується справа наліво, тому ліва catch_1виконується останньою і ловить stderr.

Найгірше, що може статися, - це те, що з’являються деякі тимчасові файли /tmp/, але вони завжди порожні. (Вони видаляються до того, як заповняться.). Зазвичай це не повинно бути проблемою, оскільки під Linux tmpfs підтримує приблизно 128K файлів на ГБ основної пам'яті.

  • Дана команда може також отримати доступ до всіх локальних змінних оболонки та змінити їх. Тож ви можете викликати функцію оболонки, яка має побічні ефекти!

  • Це лише два рази для tempfileвиклику.

Помилки:

  • Відсутність належної обробки помилок у випадку tempfileневдачі.

  • Це робить звичайне \nвидалення оболонки. Див. Коментар у catch_read().

  • Ви не можете використовувати дескриптор файлу 66для передачі даних до вашої команди. Якщо вам це потрібно, використовуйте інший дескриптор для переспрямування, наприклад 42(зауважте, що дуже старі оболонки пропонують лише FD до 9).

  • Це не може обробляти байти NUL ( $'\0') у stdoutта stderr. (NUL просто ігнорується. У readваріанті все, що стоїть за NUL, ігнорується.)

FYI:

  • Unix дозволяє нам отримувати доступ до видалених файлів, якщо ви постійно тримаєте посилання на них (наприклад, відкритий маніпулятор). Таким чином ми можемо їх відкрити, а потім видалити.

4

Не сподобався eval, тому ось рішення, яке використовує деякі прийоми перенаправлення, щоб перехопити вихідні дані програми до змінної, а потім аналізує цю змінну для вилучення різних компонентів. Прапор -w встановлює розмір фрагменту та впливає на впорядкування повідомлень std-out / err у проміжному форматі. 1 дає потенційно високу роздільну здатність за рахунок накладних витрат.

#######                                                                                                                                                                                                                          
# runs "$@" and outputs both stdout and stderr on stdin, both in a prefixed format allowing both std in and out to be separately stored in variables later.                                                                  
# limitations: Bash does not allow null to be returned from subshells, limiting the usefullness of applying this function to commands with null in the output.                                                                   
# example:                                                                                                                                                                                                                       
#  var=$(keepBoth ls . notHere)                                                                                                                                                                                                  
#  echo ls had the exit code "$(extractOne r "$var")"                                                                                                                                                                            
#  echo ls had the stdErr of "$(extractOne e "$var")"                                                                                                                                                                            
#  echo ls had the stdOut of "$(extractOne o "$var")"                                                                                                                                                                            
keepBoth() {                                                                                                                                                                                                                     
  (                                                                                                                                                                                                                              
    prefix(){                                                                                                                                                                                                                    
      ( set -o pipefail                                                                                                                                                                                                          
        base64 -w 1 - | (                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
          while read c                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
          do echo -E "$1" "$c"                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
          done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
        )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
      )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    ( (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
        "$@" | prefix o >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
        echo  ${PIPESTATUS[0]} | prefix r >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                           
      ) 2>&1 | prefix e >&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    ) 3>&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
  )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

extractOne() { # extract                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  echo "$2" | grep "^$1" | cut --delimiter=' ' --fields=2 | base64 --decode -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

2

Я думаю, що відповідь - ні. Захоплення $( ... )фіксує лише стандартний вивід змінної; немає способу отримати стандартну помилку, зафіксовану в окремій змінній. Отже, те, що у вас є, приблизно таке ж акуратне, наскільки воно стає.


1
@ormaaj: З огляду на відповіді на відповідь eval, здається, це насправді можливо, але, як ви зазначаєте , це в основному зводиться до того, щоб " використовувати кращу оболонку або мову ". Це не безпосередня відповідь на запитання, але я прийшов сюди з тим самим питанням, і я думаю, що довгостроково я перейду на оболонку, засновану на функціональній мові, такій як Haskell .
James Haigh

2

А як щодо ... = D

GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
    GET_STDERR=""
    GET_STDOUT=""
    unset t_std t_err
    eval "$( (eval $1) 2> >(t_err=$(cat); typeset -p t_err) > >(t_std=$(cat); typeset -p t_std) )"
    GET_STDERR=$t_err
    GET_STDOUT=$t_std
}

get_stderr_stdout "command"
echo "$GET_STDERR"
echo "$GET_STDOUT"

3
Здається, це обгортка навколо першої відповіді , яка не додає жодної нової функціональності. Чим це відрізняється / корисніше?
ntc2

0

Якщо команда 1) без побічних ефектів, що викликає стан, і 2) обчислювально дешева, найпростішим рішенням є просто запустити її двічі. Я в основному використовував це для коду, який працює під час послідовності завантаження, коли ви ще не знаєте, чи буде диск працювати. У моєму випадку це було крихітне, some_commandтому не було жодного показника продуктивності для запуску двічі, і команда не мала побічних ефектів.

Основна перевага полягає в тому, що це чисто та легко читається. Рішення тут досить розумні, але я б не хотів бути тим, хто повинен підтримувати сценарій, що містить більш складні рішення. Я б рекомендував простий підхід "два рази", якщо ваш сценарій працює з цим, оскільки він набагато чистіший і простіший в обслуговуванні.

Приклад:

output=$(getopt -o '' -l test: -- "$@")
errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null)
if [[ -n "$errout" ]]; then
        echo "Option Error: $errout"
fi

Знову ж таки, це нормально робити, оскільки getopt не має побічних ефектів. Я знаю, що це безпечно для роботи, оскільки мій батьківський код викликає це менше 100 разів протягом усієї програми, і користувач ніколи не помітить 100 дзвінків getopt проти 200 дзвінків getopt.


Не могли б ви навести приклад? Я здогадуюсь про щось на зразок out=$(some_command)і err=$(some_command 2>&1 1>/dev/null)?
ntc2

@eicto - тоді вам доведеться використати одне з наведених вище рішень - це хороше рішення, лише якщо ваша команда не має побічних ефектів і є обчислювально дешевою
Hamy,

1
Я сумніваюся, що існує багато випадків використання, які вимагають окремої обробки stdoutта stderrбез побічних ефектів - навіть якщо команда є детермінованою за звичайних обставин, помилки не є звичайними обставинами. Цей підхід також, ймовірно, буде схильний до умов перегонів.
James Haigh

0

Ось більш простий варіант, який не зовсім те, що хотів ОП, але на відміну від будь-якого іншого варіанту. Ви можете отримати все, що завгодно, переставивши дескриптори файлів.

Тестова команда:

%> cat xx.sh  
#!/bin/bash
echo stdout
>&2 echo stderr

що саме по собі робить:

%> ./xx.sh
stdout
stderr

Тепер надрукуйте stdout, захопіть stderr у змінну та увійдіть stdout у файл

%> export err=$(./xx.sh 3>&1 1>&2 2>&3 >"out")
stdout
%> cat out    
stdout
%> echo
$err 
stderr

Або реєструйте stdout і захопіть stderr до змінної:

export err=$(./xx.sh 3>&1 1>out 2>&3 )
%> cat out
stdout
%> echo $err
stderr

Ви зрозуміли ідею.


0

Одним із обхідних шляхів, який є хитрим, але, можливо, більш інтуїтивним, ніж деякі пропозиції на цій сторінці, є позначення вихідних потоків, їх об’єднання та подальший розподіл на основі тегів. Наприклад, ми можемо позначати stdout префіксом "STDOUT":

function someCmd {
    echo "I am stdout"
    echo "I am stderr" 1>&2
}

ALL=$({ someCmd | sed -e 's/^/STDOUT/g'; } 2>&1)
OUT=$(echo "$ALL" | grep    "^STDOUT" | sed -e 's/^STDOUT//g')
ERR=$(echo "$ALL" | grep -v "^STDOUT")

``

Якщо ви знаєте, що stdout та / або stderr мають обмежену форму, ви можете придумати тег, який не суперечить дозволеному вмісту.


Зробив більш загальний спосіб зробити це, який працює для всіх результатів, див. Мою відповідь на це запитання.
mncl

Чи є цей ризик, що sedінтерпретує вихід someCmd? Потенційне небажане виконання коду?
adrelanos

@adrelanos AFAIK у наведених вище прикладах sedбуде інтерпретувати лише рядкові аргументи, тобто s/^/STDOUT/gі s/^STDOUT//g. Оскільки вони виправлені, відомі рядки не мають вектора введення / небажаного виконання. Stdout і stderr of someCmdбудуть протікати через stdin і stdout of sed; вони будуть відредаговані, але не виконані. Так само для дзвінків до grep.
Warbo

@adrelanos Зауважте, що я припускаю, що stdout і stderr з someCmdніколи не будуть містити рядок, що починається з тексту "дозорного" STDOUT. Якщо це не підтримує, ми можемо вибрати іншого сторожа; але якщо вихідні дані є довільними (наприклад, визначеними користувачем), тоді цей метод використовувати не можна, оскільки немає можливості відрізнити будь-який сторожовий текст від даних.
Warbo,

0

ПОПЕРЕДЖЕННЯ: НЕ (ще?) РОБОТА!

Наступне здається можливим приводом для його роботи без створення тимчасових файлів, а також лише на POSIX sh; він вимагає base64, однак і через кодування / декодування може бути не таким ефективним і використовувати також "більшу" пам'ять.

  • Навіть у простому випадку це вже не вдалося б, коли останній рядок stderr не має нового рядка. Це можна виправити принаймні в деяких випадках заміною exe на "{exe; echo> & 2;}", тобто додаванням нового рядка.
  • Однак головна проблема полягає в тому, що все здається шаленим. Спробуйте використовувати exe, наприклад:

    exe () {cat /usr/share/hunspell/de_DE.dic cat /usr/share/hunspell/en_GB.dic> & 2}

і ви побачите, що, наприклад, частини кодованого рядка base64 знаходяться у верхній частині файлу, частини в кінці, а не декодовані речі stderr посередині.

Ну, навіть якщо наведена нижче ідея не може змусити працювати (що я припускаю), вона може слугувати прикладом проти людей, які можуть помилково вважати, що її можна зробити такою.

Ідея (або антиприклад):

#!/bin/sh

exe()
{
        echo out1
        echo err1 >&2
        echo out2
        echo out3
        echo err2 >&2
        echo out4
        echo err3 >&2
        echo -n err4 >&2
}


r="$(  { exe  |  base64 -w 0 ; }  2>&1 )"

echo RAW
printf '%s' "$r"
echo RAW

o="$( printf '%s' "$r" | tail -n 1 | base64 -d )"
e="$( printf '%s' "$r" | head -n -1  )"
unset r    

echo
echo OUT
printf '%s' "$o"
echo OUT
echo
echo ERR
printf '%s' "$e"
echo ERR

дає (із виправленням stderr-newline):

$ ./ggg 
RAW
err1
err2
err3
err4

b3V0MQpvdXQyCm91dDMKb3V0NAo=RAW

OUT
out1
out2
out3
out4OUT

ERR
err1
err2
err3
err4ERR

(Принаймні на тире та баш Debian)


0

Ось варіант рішення @madmurphy, який повинен працювати для довільно великих потоків stdout / stderr, підтримувати вихідне значення повернення та обробляти нулі в потоці (шляхом перетворення їх у нові рядки)

function buffer_plus_null()
{
  local buf
  IFS= read -r -d '' buf || :
  echo -n "${buf}"
  printf '\0'
}

{
    IFS= time read -r -d '' CAPTURED_STDOUT;
    IFS= time read -r -d '' CAPTURED_STDERR;
    (IFS= read -r -d '' CAPTURED_EXIT; exit "${CAPTURED_EXIT}");
} < <((({ { some_command ; echo "${?}" 1>&3; } | tr '\0' '\n' | buffer_plus_null; } 2>&1 1>&4 | tr '\0' '\n' | buffer_plus_null 1>&4 ) 3>&1 | xargs printf '%s\0' 1>&4) 4>&1 )

Мінуси:

  • Ці readкоманди є найдорожчою частиною операції. Наприклад: find /procна комп'ютері, на якому запущено 500 процесів, потрібно 20 секунд (тоді як команда була лише 0,5 секунди). Перший час читання займає 10 секунд, а другий - 10 секунд, подвоюючи загальний час.

Пояснення буфера

Оригінальне рішення полягало в аргументі для printfбуферизації потоку, однак із необхідністю мати код виходу останнім, одним із рішень є буферизація як stdout, так і stderr. Я спробував, xargs -0 printfале тоді ви швидко почали натискати "максимальна довжина аргументу". Тому я вирішив, що рішенням є написання функції швидкого буфера:

  1. Використовуйте readдля зберігання потоку у змінній
  2. Це readзавершиться, коли потік закінчиться, або буде отримано нуль. Оскільки ми вже видалили нулі, він закінчується, коли потік закритий, і повертає ненульове значення. Оскільки це очікувана поведінка, ми додаємо || :значення "або true", щоб рядок завжди отримував значення true (0)
  3. Тепер, коли я знаю, що потік закінчився, можна починати повторювати його повторно.
  4. echo -n "${buf}" є вбудованою командою і, отже, не обмежується обмеженням довжини аргументу
  5. Нарешті, додайте нульовий роздільник до кінця.

0

Виведення в реальному часі та запис у файл:

#!/usr/bin/env bash

# File where store the output
log_file=/tmp/out.log

# Empty file
echo > ${log_file}

outToLog() {
  # File where write (first parameter)
  local f="$1"
  # Start file output watcher in background
  tail -f "${f}" &
  # Capture background process PID
  local pid=$!
  # Write "stdin" to file
  cat /dev/stdin >> "${f}"
  # Kill background task
  kill -9 ${pid}
}

(
  # Long execution script example
  echo a
  sleep 1
  echo b >&2
  sleep 1
  echo c >&2
  sleep 1
  echo d
) 2>&1 | outToLog "${log_file}"

# File result
echo '==========='
cat "${log_file}"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.