我认为在说“你不能”做某事之前,人们至少应该亲手尝试一下......
简单干净的解决方案,无需使用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
and stderr
: useless.sh
#!/bin/bash
#
# useless.sh
#
echo "This is stderr" 1>&2
echo "This is stdout"
将捕获的实际脚本stdout
and 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_command
to printf '\0%s\0'
,从而创建字符串\0${stdout}\n\0
(where \0
is a 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
byte 并将内容保存到${CAPTURED_STDERR}
。然后命令
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
继续读取相同的字符串直到下一个NUL
byte 并将内容保存到${CAPTURED_STDOUT}
.
4. 使其坚不可摧
上面的解决方案依赖于NUL
之间的分隔符字节stderr
and stdout
,因此如果出于任何原因它将无法工作stderr
包含其他NUL
bytes.
尽管这种情况很少发生,但通过剥离所有可能的情况,可以使脚本完全牢不可破。NUL
字节来自stdout
and stderr
在将两个输出传递给之前read
(消毒)-NUL
字节无论如何都会丢失,因为不可能将它们存储到 shell 变量中 https://stackoverflow.com/questions/6570531/assign-string-containing-null-character-0-to-a-variable-in-bash/24511770#24511770:
{
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
5. 保留退出状态 – 蓝图(未经清理)
在思考了最终的方法之后,我想出了一个使用的解决方案printf
缓存both stdout
和退出代码作为两个不同的参数,这样它们就不会干扰。
我做的第一件事是概述一种将退出状态传达给第三个参数的方法printf
,这是非常容易以最简单的形式完成的事情(即无需消毒)。
{
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
6. 通过清理保留退出状态 – 牢不可破(重写)
然而,当我们尝试引入消毒措施时,事情会变得非常混乱。发射tr
清理流实际上会覆盖我们之前的退出状态,因此显然唯一的解决方案是在丢失之前将后者重定向到一个单独的描述符,将其保留在那里直到tr
它的工作执行两次,然后将其重定向回原来的位置。
在文件描述符之间进行一些相当复杂的重定向之后,这就是我得出的结果。
下面的代码是对前面示例的重写(您可以在下面的附录中找到它)。它还可以消毒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 [ARG1[ ARG2[ ...[ ARGN]]]]
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' "$(((({ shift 2; "${@}"; 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
, shift
, tr
变更日志: 2022-06-17 // 已替换${3}
with shift 2; ${@}
after 帕维尔·坦科夫的评论 https://stackoverflow.com/questions/11027679/capture-stdout-and-stderr-into-different-variables/59592881?noredirect=1#comment126850672_59592881(仅限 Bash)。 2023-01-18 // 已替换${@}
with "${@}"
after cbugk的评论 https://stackoverflow.com/questions/11027679/capture-stdout-and-stderr-into-different-variables/59592881?noredirect=1#comment131497771_59592881.
随着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
的退出代码重定向到描述符 3
-
stdout
被传送到tr
(消毒)
-
stderr
被交换为stdout
(暂时使用描述符 4)并通过管道传送到tr
(消毒)
- 退出代码(描述符 3)被交换为
stderr
(现在描述符 1)并通过管道传输到exit $(cat)
-
stderr
(现在描述符 3)被重定向到描述符 1,end 扩展为第二个参数printf
- 的退出代码
exit $(cat)
被第三个参数捕获printf
- 的输出
printf
被重定向到描述符 2,其中stdout
已经存在
- 的串联
stdout
和输出printf
被传送到read
9. POSIX 兼容版本#1(易破解)
流程替代 https://www.gnu.org/software/bash/manual/html_node/Process-Substitution.html (the < <()
语法)不是 POSIX 标准(尽管它们de facto是)。在不支持的 shell 中< <()
语法达到相同结果的唯一方法是通过<<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
Note: As shift
仅限 Bash,在此 POSIX 兼容版本中,命令 + 参数必须出现在相同的引号下。
10. POSIX 兼容版本#2(牢不可破,但不如非 POSIX 版本)
这是它的牢不可破的版本,直接以函数形式(如果stdout
or 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
答案的历史
这是以前的版本catch()
before 帕维尔·坦科夫的注释(此版本需要将附加参数与命令一起引用):
# SYNTAX:
# catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND [ARG1[ ARG2[ ...[ ARGN]]]]
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' "$(((({ shift 2; ${@}; 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
此外,我替换了一个将退出状态传播到当前 shell 的旧示例,因为,Andy在评论中指出,它并不像想象的那样“牢不可破”(因为它没有使用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
Later, Andy提交了以下“建议编辑”来捕获退出代码:
简单干净的解决方案,节省退出价值
我们可以在末尾添加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
of some_command
,我们无法再进行消毒了NUL
字节数stderr
,因为现在其中还有our NUL
分隔符。
试图找到这个问题的正确解决方案是我写下这篇文章的原因§ 5. 保留退出状态 – 蓝图(未经清理),以及以下部分。