我认为在说“你不能”做某事之前,人们至少应该亲手尝试一下……
简单而干净的解决方案,不使用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_command的标准输出发送到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 字节,它将不起作用。
尽管这永远不会发生,但可以通过从stdout 和stderr 中剥离所有可能的NUL 字节,然后将两个输出传递给read(清理),从而使脚本完全牢不可破——NUL 字节反正会迷路,就像it is not possible to store them into shell variables:
{
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
编辑
我删除了将退出状态传播到当前 shell 的另一个示例,因为正如 Andy 在 cmets 中指出的那样,它并不像预期的那样“牢不可破” (因为它没有使用printf 来缓冲其中一个流)。作为记录,我将有问题的代码粘贴在这里:
保持退出状态(仍然牢不可破)
以下变体还将some_command 的退出状态传播到当前shell:
{
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
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 的输出立即附加到some_command 的stderr,我们不能再清理stderr 中可能的NUL 字节,因为其中现在还有我们的 NUL 分隔符。
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
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函数我们可以启动以下sn-p,
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 启动:然后我们在描述符 1 上有 some_command 的 stdout,在描述符 2 上有 some_command 的 stderr 和 some_command 的退出代码重定向到描述符 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。符合 POSIX 的版本 #1(易破解)
Process substitutions(< <() 语法)不是 POSIX 标准(尽管它们事实上是)。在不支持< <() 语法的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
10.符合 POSIX 的版本 #2(牢不可破,但不如非 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