Думаю, перед тим, як сказати «ти не можеш» щось зробити, людям слід хоча б спробувати своїми руками ...
Просте і чисте рішення, без використання 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_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. Кінцеве рішення - функція загального призначення зі статусом виходу
Ми також можемо перетворити наведений вище код у загальну функцію.
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