Думаю, перед тим, як сказати «ти не можеш» щось зробити, людям слід хоча б спробувати своїми руками ...
Просте і чисте рішення, без використання 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
echo "This is stderr" 1>&2
echo "This is stdout"
Фактичний скрипт, який буде фіксувати stdout
та stderr
:capture.sh
#!/bin/bash
{
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_command
to 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. Кінцеве рішення - функція загального призначення зі статусом виходу
Ми також можемо перетворити наведений вище код у загальну функцію.
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. Що відбувається в останніх прикладах
Тут слід швидка схематизація:
some_command
запускається: ми тоді маємо some_command
«S stdout
на дескриптор 1, some_command
" S stderr
на дескриптор 2 і some_command
«s код виходу перенаправлені на дескриптор 3
stdout
підводиться до tr
(санітарія)
stderr
замінюється на stdout
(тимчасово використовуючи дескриптор 4) і передається доtr
(санітарія)
- код виходу (дескриптор 3) міняється місцями
stderr
(тепер дескриптор 1) і передається доexit $(cat)
stderr
(тепер дескриптор 3) перенаправляється на дескриптор 1, кінець розширений як другий аргумент printf
- код виходу
exit $(cat)
фіксується третім аргументомprintf
- вихідні дані
printf
переспрямовуються до дескриптора 2, де stdout
він уже був присутній
- конкатенація
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'
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