【问题标题】:Capture stdout and stderr into different variables将 stdout 和 stderr 捕获到不同的变量中
【发布时间】:2012-06-17 03:59:31
【问题描述】:

是否可以在不同的变量中存储或捕获标准输出和标准错误,而不使用临时文件?现在我这样做是为了在运行some_command 时获得out 中的stdout 和err 中的stderr,但我会 喜欢避免临时文件。

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

【问题讨论】:

  • This question 包含我的问题,但没有回答。
  • fwiw,在 ksh93 中是可能的。 ksh -c 'function f { echo out; echo err &gt;&amp;2; }; x=${ { y=$(f); } 2&gt;&amp;1;}; typeset -p x y'
  • @ormaaj:你能解释一下 ksh93 技术是如何工作的吗?您可能需要将其作为答案。
  • @gniourf_gniourf 嗯,这个问题在这个问题之后,所以,如果有重复,应该是那个:capture both stdout and stderr in bash:-D

标签: bash shell command-line io-redirection


【解决方案1】:

好的,它有点难看,但这里有一个解决方案:

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 &gt;&amp;2) 需要替换为实际命令。 stdout 的输出逐行保存到数组 $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

使用 GNU bash 在 Debian wheezy 上测试,版本 4.2.37(1)-release (i486-pc-linux-gnu)

【讨论】:

  • 这就是为什么我会以同样的方式处理返回。试试eval "$( eval "$@" 2&gt; &gt;(t_err=$(cat); typeset -p t_err) &gt; &gt;(t_std=$(cat); typeset -p t_std); t_ret=$?; typeset -p t_ret )"; exit $t_ret
  • 感谢您的概念。我在这里扩展(提炼)了一点:stackoverflow.com/a/28796214/2350426
  • typeset -p t_outtypeset -p t_err 可能混合在一起,导致输出无用。
  • @4ae1e1 我想过这种可能性,但无法确认这是否会发生。
  • @TheConstructor 嗯,我认为你是对的。我在 Zsh 中使用 &gt;&gt;() 而不是 &gt; &gt;()。前者在 Bash 中是不行的;在 Zsh 中,它正确解析出进程替换部分,但有时会发出损坏的输出。不知道为什么,但&gt; &gt;() 似乎工作可靠。我仍然不完全相信。 typeset -p 绝对不是原子的,是吗?
【解决方案2】:

这是为了在不同的变量中捕获标准输出和标准错误。 If you only want to catch stderr, leaving stdout as-is, there is a better and shorter solution.

为了读者的利益,致sum一切up,这是一个

易于重复使用bash 解决方案

此版本确实使用子shell,并且在没有tempfiles 的情况下运行。 (对于没有子shell 的tempfile 版本,请参阅my other answer。)

: 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。真的没有什么复杂的。

附录:在“strict mode”中使用

catch 在严格模式下同样适用于我。唯一需要注意的是,上面的示例返回错误代码 3,在严格模式下,它调用 ERR 陷阱。因此,如果您在set -e 下运行一些预期会返回任意错误代码(不仅仅是0)的命令,您需要将返回代码捕获到某个变量中,例如 &amp;&amp; ret=$? || ret=$?,如下所示:

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

catch stdout stderr dummy 3 $'\ndifficult\n data \n\n\n' $'\nother\n difficult \n  data  \n\n' && ret=$? || ret=$?

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

讨论

问:它是如何工作的?

它只是将此处其他答案的想法包装到一个函数中,以便可以轻松地重复使用。

catch() 基本上使用eval 来设置这两个变量。这类似于https://stackoverflow.com/a/18086548

考虑拨打catch out err dummy 1 2a 3b

  • 让我们暂时跳过eval "$({__2="$(。我稍后会谈到这个。

  • __1="$("$("${@:3}")"; } 2&gt;&amp;1; 执行dummy 1 2a 3b 并将其stdout 存储到__1 以供以后使用。所以__1 变成了2a。它还将dummystderr 重定向到stdout,这样外部的catch 就可以聚集stdout

  • ret=$?; 捕获退出代码,即1

  • printf '%q=%q\n' "$1" "$__1" &gt;&amp;2; 然后将out=2a 输出到stderr。这里使用stderr,因为当前的stdout已经接管了dummy命令的stderr的角色。

  • exit $ret 然后将退出代码 (1) 转发到下一阶段。

现在到外面__2="$( ... )"

  • 这会将上述stdout(即dummy 调用的stderr)捕获到变量__2 中。 (我们可以在这里重复使用__1,但我使用__2 以减少混乱。)。所以__2变成3b

  • ret="$?"; 再次捕获(返回的)返回码 1(来自 dummy

  • printf '%s=%q\n' "$2" "$__2" &gt;&amp;2; 然后将err=3a 输出到stderr。再次使用stderr,因为它已经用于输出另一个变量out=2a

  • printf '( exit %q )' "$ret" &gt;&amp;2; 然后输出代码以设置正确的返回值。我没有找到更好的方法,因为将其分配给变量需要一个变量名,然后不能将其用作catch 的第一个或第二个参数。

请注意,作为优化,我们也可以将这 2 个 printf 写成一个单独的 printf '%s=%q\n( exit %q ) "$__2" "$ret"`。

那么到目前为止我们有什么?

我们已将以下内容写入标准错误:

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

其中out 来自$12a 来自dummystdouterr 来自$23b 来自stderrdummy1 来自dummy 的返回码。

请注意,printf 格式的 %q 会小心引用,这样当涉及到 eval 时,shell 会看到正确的(单个)参数。 2a3b 是如此简单,以至于它们被逐字复制。

现在到外面eval "$({ ... } 2&gt;&amp;1 )";

这将执行以上所有输出 2 个变量和 exit,捕获它(因此是 2&gt;&amp;1)并使用 eval 将其解析到当前 shell。

这样设置了 2 个变量和返回码。

问:它使用了邪恶的eval。那么它安全吗?

  • 只要printf %q 没有错误,它应该是安全的。但是您始终必须非常小心,想想 ShellShock。

问:错误?

  • 没有明显的已知错误,除了以下:

    • 捕获大输出需要大内存和 CPU,因为所有内容都进入变量并需要由 shell 进行反向解析。所以要明智地使用它。

    • 像往常一样$(echo $'\n\n\n\n') 吞下所有换行符,不仅仅是最后一个。这是 POSIX 要求。如果您需要让 LF 不受伤害,只需在输出中添加一些尾随字符,然后像下面的配方一样将其删除(查看尾随 x,它允许读取指向以 $'\n' 结尾的文件的软链接):

          target="$(readlink -e "$file")x"
          target="${target%x}"
      
    • Shell 变量不能携带字节 NUL ($'\0')。如果它们恰好出现在 stdoutstderr 中,它们就会被忽略。

  • 给定的命令在子子shell中运行。所以它不能访问$PPID,也不能改变shell变量。您可以 catch 一个 shell 函数,甚至是内置函数,但这些函数将无法更改 shell 变量(因为在 $( .. ) 中运行的所有内容都无法做到这一点)。因此,如果您需要在当前 shell 中运行一个函数并捕获它的 stderr/stdout,您需要使用tempfiles 以通常的方式执行此操作。 (有很多方法可以做到这一点,打断 shell 通常不会留下碎片,但这很复杂,值得自己回答。)

问:Bash 版本?

  • 我认为您需要 Bash 4 及更高版本(由于 printf %q

问:这看起来还是很别扭。

  • 没错。 Another answer here 展示了如何在ksh 中更干净地完成它。不过我不习惯ksh,所以我把它留给其他人为ksh 创建一个类似的易于重用的配方。

问:那为什么不用ksh呢?

  • 因为这是bash 解决方案

问:脚本可以改进

  • 当然,您可以挤出一些字节并创建更小或更难以理解的解决方案。去吧;)

问:有一个错字。 : catch STDOUT STDERR cmd args.. 应改为 # catch STDOUT STDERR cmd args..

  • 实际上这是有意的。 : 出现在 bash -x 中,而 cmets 被默默吞下。因此,如果您碰巧在函数定义中出现拼写错误,您可以看到解析器在哪里。这是一个古老的调试技巧。但请注意,您可以在 : 的参数中轻松创建一些简洁的副作用。

编辑:添加了更多;,以便更轻松地从catch() 创建单行。并添加了它的工作原理部分。

【讨论】:

  • 这是一个非常有趣的解决方案,因为它使这种用法更易于使用。但是,考虑到它不遵循其他提议的解决方案的一般模式,您应该提供更多关于它如何工作的细节。
  • 对于重定向流之一或管道的命令如何catch?尝试捕获两个输出似乎是有问题的,因为其中一个是空的(因为命令本身无论如何都会重定向它)。但是,即使在某些情况下,其中一个变量注定为空,它也可以更轻松地对任何命令一遍又一遍地使用相同的模式(特别是如果该命令是外部提供的并且您不知道它是否重定向)。
  • 到目前为止,我找到了一个简单的解决方法。只需定义一个像function echo_to_file { echo -n "$1" &gt;"$2" ; } 这样的简单函数,然后将catch 与该函数一起使用。按预期工作。但是,将它放在catch 本身中仍然会很好。 (类似的“技巧”可以在命令中使用管道。)
  • 在函数catch中,最后的printf语句不应该是printf 'return %q\n' "$ret" &gt;&amp;2吗?希望函数catch 返回cmd 的退出代码,而不是退出程序。
  • 可以修复它在“严格模式”下工作吗?它在} 2&gt;&amp;1 )"; 行出错。我使用这个严格模式的定义:set -eEu -o pipefail; shopt -s extdebug; IFS=$'\n\t'; trap 'wickStrictModeFail $?' ERR,可在github.com/tests-always-included/wick/blob/master/doc/…获得
【解决方案3】:

我认为在说“你不能”做某事之前,人们至少应该亲手尝试一下……

简单而干净的解决方案,不使用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。一个简单的测试

用于生成stdoutstderr 的虚拟脚本:useless.sh

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

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

将捕获stdoutstderr 的实际脚本: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(其中\0NUL字节,\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 字节作为stderrstdout 之间的分隔符,因此如果出于任何原因stderr 包含其他NUL 字节,它将不起作用。

尽管这永远不会发生,但可以通过从stdoutstderr 中剥离所有可能的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)

要求: printfreadtr

编辑

我删除了将退出状态传播到当前 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-)

要求: printfreadtrxargs

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_commandstderr,我们不能再清理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)

要求: exitprintfread

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)

要求: exitprintfreadtr

这个解决方案非常强大。退出代码始终保存在不同的描述符中,直到它作为单独的参数直接到达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)
}

要求:catexitprintfreadtr

使用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。最后一个例子发生了什么

下面是一个快速的模式化:

  1. some_command 启动:然后我们在描述符 1 上有 some_commandstdout,在描述符 2 上有 some_commandstderrsome_command 的退出代码重定向到描述符 3
  2. stdout 通过管道传输到 tr(清理)
  3. stderrstdout 交换(暂时使用描述符 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。符合 POSIX 的版本 #1(易破解)

Process substitutions&lt; &lt;() 语法)不是 POSIX 标准(尽管它们事实上是)。在不支持&lt; &lt;() 语法的shell 中,获得相同结果的唯一方法是通过&lt;&lt;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

要求: exitprintfread

10.符合 POSIX 的版本 #2(牢不可破,但不如非 POSIX 版本)

这是它的牢不可破的版本,直接以函数形式(如果stdoutstderr 包含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
}

要求: catcutexitprintfreadtr

【讨论】:

  • 这很聪明。
  • 好点。您的解决方案有效,但有一个小问题,即退出状态应该代表字符串的最后一段,如果我们希望能够在圆括号内执行 exit "${CAPTURED_EXIT}" 以不污染全局范围,就像我试图做的那样我的最后一个例子。另一个问题是,由于最里面的printf 的输出立即附加到some_commandstderr,我们不能再清理stderr 中可能的NUL 字节,因为其中还有 我们的 NUL 分隔符。我会在接下来的几天里考虑一些事情。
  • 这是天才...很棒的工作...只是澄清一下,通过tr 的最终解决方案从每个流中完全删除所有\0 字节是否正确?
  • @Angel 谢谢你的话。我假设您在谈论第 6 和第 7 段;是的,如果您不通过tr 删除所有\0 字节,那么只要您尝试将字符串存储到shell 变量中,它们就会被删除。 Shell 变量不能包含 NUL 字节。
  • @madmurphy 这是使用重定向的非常聪明的方法!我刚刚发布了一个带有图表的社区 wiki 答案,以显示正在发生的事情。随意使用它。如果您想更改某些内容,我可以做到或将 svg 文件发送给您。
【解决方案4】:

从技术上讲,命名管道不是临时文件,这里没有人提到它们。它们在文件系统中不存储任何内容,您可以在连接它们后立即删除它们(因此您永远不会看到它们):

#!/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

您可以通过这种方式拥有多个后台进程,并在方便的时候异步收集它们的标准输出和标准错误等。

如果您只需要一个进程,您也可以使用硬编码的 fd 数字,例如 3 和 4,而不是 {fdout}/{fderr} 语法(它会为您找到一个免费的 fd)。

【讨论】:

  • 您需要等待foo &amp; 子shell 完成以获取它的退出代码。例如,foo &gt;stdout 2&gt;stderr &amp; pid=$!; exec {fdout}&lt;stdout {fderr}&lt;stderr; wait $pid; rc=$?
  • 这帮助我最终解决了 git stdout 和 stderr 结合对话框 prgbox 的分离处理,非常感谢!
【解决方案5】:

此命令在当前运行的 shell 中设置 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"

要执行该代码(并获取变量集),请使用 eval:

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

最后回显设置的变量:

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|"

【讨论】:

  • 存在 竞争条件,因为当整个输出长度超过 1008 字节时,declare 不会进行原子写入(Ubuntu 16.04,bash 4.3.46(1))。在 stdout 和 stderr 的两个 setval 调用之间存在隐式同步(setval for stderr 中的 cat 在 stdout 的 setval 关闭 stderr 之前无法完成)。但是,setval retval 没有同步,因此它可以介于两者之间。在这种情况下,retval 被其他两个变量之一吞噬。所以retval 的情况下运行不可靠。
  • 我想我喜欢这种方法.. 有点。有没有办法将该 eval 移动到单独的函数并将命令传递给它?当我尝试这样做时,它不会声明 errval 或 stdval。
  • 我制作了capturable(){...}(设置为书面形式)和capture(){ eval "$( $@ 2&gt; &gt;(capturable stderr) &gt; &gt;(capturable stdout); )"; test -z "$stderr" }capture make ... &amp;&amp; echo "$stdout" || less &lt;&lt;&lt;"$stderr" 分页 stderr 或打印 stdout(如果没有)。这对你有用吗,或者如果有用的话对你有帮助吗?
【解决方案6】:

乔纳森有the answer。作为参考,这是 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;} 语法只是一个不会创建子 shell 的命令替换。这些命令在当前 shell 环境中执行。开头的空格很重要({ 是保留字)。

内部命令组的Stderr 被重定向到stdout(因此它适用于内部替换)。接下来,out 的 stdout 被分配给 y,重定向的 stderr 被x 捕获,而不会像往常一样将y 丢失到命令替换的子shell。

这在其他 shell 中是不可能的,因为所有捕获输出的构造都需要将生产者放入子 shell 中,在这种情况下,子shell 将包括分配。

更新:现在 mksh 也支持了。

【讨论】:

  • 谢谢。关键是${ ... } 不是子shell,这使得其余部分易于解释。巧妙的技巧,只要你有一个 ksh 可以使用。
  • 这不是问题的答案。问题是关于 bash,而您的答案在 ksh 上有效。
  • @mshamma 显然。阅读最后一段。
【解决方案7】:

为了读者的利益,这里有一个使用tempfiles 的解决方案。

问题是不要使用tempfiles。然而,这可能是由于 /tmp/ 对 tempfile 造成不必要的污染,以防 shell 死机。在kill -9 的情况下,一些trap 'rm "$tmpfile1" "$tmpfile2"' 0 不会触发。

如果您可以使用tempfile,但又希望永远不要留下碎片,这里有一个秘诀。

它再次被称为catch()(就像我的other answer)并且具有相同的调用语法:

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}";
}

它的作用:

  • 它为stdoutstderr 创建了两个tempfiles。然而,它几乎立即删除了这些,因此它们只存在很短的时间。

  • catch_1()stdout (FD 1) 捕获到一个变量中并将stderr 移动到stdout,这样下一个(“左”)catch_1 可以捕获它。

  • catch 中的处理是从右到左完成的,所以左边的catch_1 最后执行并捕获stderr

可能发生的最糟糕的情况是,/tmp/ 上显示了一些临时文件,但在这种情况下它们总是空的。 (它们在填充之前被移除。)。通常这应该不是问题,因为在 Linux 下,tmpfs 支持每 GB 主内存大约 128K 文件。

  • 给定的命令也可以访问和更改所有本地 shell 变量。所以你可以调用一个有副作用的shell函数!

  • 这只会为tempfile 调用分叉两次。

错误:

  • tempfile 失败的情况下缺少良好的错误处理。

  • 这将执行通常的\n 删除外壳程序。请参阅catch_read() 中的评论。

  • 您不能使用文件描述符 66 将数据通过管道传输到您的命令。如果需要,请使用另一个描述符进行重定向,例如 42(请注意,非常旧的 shell 最多只能提供 9 个 FD)。

  • 这无法处理 stdoutstderr 中的 NUL 字节 ($'\0')。 (NUL 被忽略。对于read 变体,NUL 后面的所有内容都被忽略。)

仅供参考:

  • Unix 允许我们访问已删除的文件,只要您保留对它们的一些引用(例如打开的文件句柄)。这样我们就可以打开然后删除它们。

【讨论】:

    【解决方案8】:

    不喜欢 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 -                                                                                                                                                                                                                                                                                                                                                                                                                           
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
    

    【讨论】:

      【解决方案9】:

      这是一张显示@madmurphy 的very neat solution 工作原理的图表。

      单行的缩进版本:

      catch() {
        {
            IFS=$'\n' read -r -d '' "$out_var";
            IFS=$'\n' read -r -d '' "$err_var";
            (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
        )
      }
      

      【讨论】:

        【解决方案10】:

        简而言之,我相信答案是“不”。捕获$( ... ) 只捕获标准输出到变量;没有办法将标准错误捕获到单独的变量中。所以,你所拥有的就是尽可能的整洁。

        【讨论】:

        • @ormaaj:从基于eval 的答案来看,似乎实际上是可能的,但是,作为you point out,它基本上归结为'使用更好的外壳或语言'。这不是问题的直接答案,但我带着同样的问题来到这里,我认为,从长远来看,我将切换到基于 functional language 的 shell,例如 Haskell
        【解决方案11】:

        那……=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"
        

        【讨论】:

        • 这似乎是 first answer 的包装器,不会添加任何新功能。这有何不同/更有用?
        【解决方案12】:

        一种解决方法是标记输出流,合并它们,然后根据标记进行拆分,这很简单,但可能比此页面上的一些建议更直观。例如,我们可以用“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 是受限制的形式,您可以想出一个与其允许的内容不冲突的标签。

        【讨论】:

        • 做了一种适用于所有输出的更通用的方法,请参阅我对这个问题的回答。
        • 这是否会导致sed 解释someCmd 的输出?潜在的不需要的代码执行?
        • @adrelanos AFAIK 在上述示例中sed 只会解释字符串参数,即s/^/STDOUT/gs/^STDOUT//g。由于这些是固定的已知字符串,因此没有注入/不需要的执行向量。 someCmd的stdout和stderr会流过sed的stdin和stdout;它们将被编辑但不会被执行。对grep 的调用也是如此。
        • @adrelanos 请注意,我假设someCmd 的标准输出和标准错误永远不会包含以“哨兵”文本STDOUT 开头的行。如果这不成立,我们可以选择不同的哨兵;但如果输出是任意(例如用户定义),则无法使用此方法,因为无法将任何标记文本与数据区分开来。
        【解决方案13】:

        警告:(还没有?)工作!

        以下似乎可能导致它在不创建任何临时文件的情况下运行,并且仅在 POSIX sh 上运行;但是它需要base64,并且由于编码/解码可能不是那么有效并且还使用“更大”的内存。

        • 即使在简单的情况下,当最后一个 stderr 行没有换行符时,它也会失败。至少在某些情况下,这可以通过将 exe 替换为“{ exe ; echo >&2 ; }”来解决,即添加换行符。
        • 然而,主要的问题是一切看起来都很活泼。尝试使用类似的 exe:

          exe() { 猫 /usr/share/hunspell/de_DE.dic 猫 /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 的 dash 和 bash 上)

        【讨论】:

          【解决方案14】:

          这是@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 将在流结束或收到空值时终止。由于我们已经删除了空值,它会在流关闭时结束,并返回非零值。由于这是预期的行为,我们添加 || : 表示“或真”,以便该行始终计算为真 (0)
          3. 现在我知道流已经结束,可以安全地开始回显它了。
          4. echo -n "${buf}" 是内置命令,因此不受参数长度限制
          5. 最后,在末尾添加一个空分隔符。

          【讨论】:

          • 对不起,我想澄清一下,原版的主要补充是什么?原始选项是否适用于相对较大的数据(即 5,000 kB)?似乎确实如此。关于速度,原始需要 5.8s,替代方案 - declare i; for (( i = 0; i &lt; 20; i++ )); do dmesg; done 代码(5338099 字符)需要 9.9s。
          【解决方案15】:

          如果命令 1) 没有状态副作用和 2) 计算成本低,最简单的解决方案是运行两次。当您还不知道磁盘是否可以工作时,我主要将其用于在引导序列期间运行的代码。在我的情况下,它是一个很小的some_command,因此运行两次不会影响性能,并且该命令没有副作用。

          主要的好处是它干净且易于阅读。这里的解决方案非常聪明,但我不想成为必须维护包含更复杂解决方案的脚本的人。如果您的方案适用于此,我建议使用简单的 run-it-twice 方法,因为它更清洁且更易于维护。

          例子:

          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&gt;&amp;1 1&gt;/dev/null)
          • @eicto - 那么你必须使用上述解决方案之一 - 如果你的命令没有副作用并且计算成本低,这只是一个很好的解决方案
          • 我怀疑是否有许多用例需要单独处理 stdoutstderr 而没有副作用——即使命令在 normal 下是确定性的i> 情况下,错误不是正常情况。这种方法也可能容易出现竞争条件。
          【解决方案16】:

          这是一个更简单的变体,它不是 OP 想要的,但与其他任何选项都不同。你可以通过重新排列文件描述符来获得任何你想要的东西。

          测试命令:

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

          它本身就是:

          %> ./xx.sh
          stdout
          stderr
          

          现在,打印标准输出,将标准错误捕获到变量中,并将标准输出记录到文件中

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

          或者记录标准输出并将标准错误捕获到一个变量中:

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

          你明白了。

          【讨论】:

            【解决方案17】:

            实时输出并写入文件:

            #!/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}"
            

            【讨论】:

              【解决方案18】:

              我已在此处发布了解决此问题的方法。它确实使用进程替换并且需要 Bash > v4,但也将 stdout、stderr 和返回代码捕获到您在当前范围内命名的变量中:

              https://gist.github.com/pmarreck/5eacc6482bc19b55b7c2f48b4f1db4e8

              这个练习的重点是让我可以在测试套件中断言这些东西。事实上,我整个下午都在弄清楚这个听起来很简单的事情......我希望这些解决方案中的一个可以帮助其他人!

              【讨论】:

                猜你喜欢
                • 2014-08-05
                • 1970-01-01
                • 2014-08-28
                • 1970-01-01
                • 1970-01-01
                • 2011-05-23
                • 2013-06-15
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多