bash 将 stdout 和 stderr 捕获到不同的变量中

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/11027679/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-09 22:15:18  来源:igfitidea点击:

Capture stdout and stderr into different variables

bashshellcommand-lineio-redirection

提问by ntc2

Is it possible to store or capture stdout and stderr in different variables, without using a temp file? Right now I do this to get stdout in outand stderr in errwhen running some_command, but I'd like to avoid the temp file.

是否可以在不使用临时文件的情况下在不同变量中存储或捕获 stdout 和 stderr ?现在我这样做是为了outerr运行时获取 stdout和 stderr some_command,但我想避免使用临时文件。

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

采纳答案by TheConstructor

Ok, it got a bit ugly, but here is a solution:

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

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

where (echo std; echo err >&2)needs to be replaced by the actual command. Output of stdoutis saved into the array $t_stdline by line omitting the newlines (the -t) and stderrinto $t_err.

where(echo std; echo err >&2)需要由实际命令替换。的输出标准输出被保存到数组中$t_std由线省略换行符(线-t)和标准错误$t_err

If you don't like arrays you can do

如果你不喜欢数组,你可以做

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

which pretty much mimics the behavior of var=$(cmd)except for the value of $?which takes us to the last modification:

这几乎模仿了var=$(cmd)除了$?which的值将我们带到最后一次修改的行为:

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

Here $?is preserved into $t_ret

这里$?保存成$t_ret

Tested on Debian wheezy using GNUbash, Version 4.2.37(1)-release (i486-pc-linux-gnu).

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

回答by Tino

This is for catching stdout and stderr in different variables. If you only want to catch stderr, leaving stdoutas-is, there is a better and shorter solution.

这是为了在不同的变量中捕获 stdout 和 stderr。 如果您只想 catch stderr,保持stdout原样,有一个更好更短的解决方案

To sumeverything upfor the benefit of the reader, here is an

总结的一切读者的利益,这里是一个

Easy Reusable bashSolution

易于重复使用的bash解决方案

This version does use subshells and runs without tempfiles. (For a tempfileversion which runs without subshells, see my other answer.)

这个版本确实使用了子外壳并且在没有tempfiles 的情况下运行。(对于tempfile没有子外壳运行的版本,请参阅我的其他答案。)

: catch STDOUT STDERR cmd args..
catch()
{
eval "$({
__2="$(
  { __1="$("${@:3}")"; } 2>&1;
  ret=$?;
  printf '%q=%q\n' "" "$__1" >&2;
  exit $ret
  )"
ret="$?";
printf '%s=%q\n' "" "$__2" >&2;
printf '( exit %q )' "$ret" >&2;
} 2>&1 )";
}

Example use:

使用示例:

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

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"

this prints

这打印

ret=3
stdout=$'\ndiffcult\n data '
stderr=$'\nother\n difficult \n  data  '

So it can be used without deeper thinking about it. Just put catch VAR1 VAR2in front of any command args..and you are done.

所以不用深入思考就可以使用。只要放在catch VAR1 VAR2any 前面,command args..你就完成了。

Some if cmd args..; thenwill become if catch VAR1 VAR2 cmd args..; then. Really nothing complex.

有些if cmd args..; then会变成if catch VAR1 VAR2 cmd args..; then. 真的没有什么复杂的。

Discussion

讨论

Q: How does it work?

问:它是如何工作的?

It just wraps ideas from the other answers here into a function, such that it can easily be resused.

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

catch()basically uses evalto set the two variables. This is similar to https://stackoverflow.com/a/18086548

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

Consider a call of catch out err dummy 1 2a 3b:

考虑一个调用catch out err dummy 1 2a 3b

  • let's skip the eval "$({and the __2="$(for now. I will come to this later.

  • __1="$("$("${@:3}")"; } 2>&1;executes dummy 1 2 3and stores it's stdoutinto __1for later use. So __1becomes 2a. It also redirects stderrof dummyto stdout, such that the outer catch can gather stdout

  • ret=$?;catches the exit code, which is 1

  • printf '%q=%q\n' "$1" "$__1" >&2;then outputs out=2ato stderr. stderris used here, as the current stdoutalready has taken over the role of stderrof the dummycommand.

  • exit $retthen forwards the exit code (1) to the next stage.

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

  • __1="$("$("${@:3}")"; } 2>&1;执行dummy 1 2 3并将它stdout变成__1供以后使用。于是__1变成2a。它还重定向stderrdummystdout,使得外键锁可以收集stdout

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

  • printf '%q=%q\n' "$1" "$__1" >&2;然后输出out=2astderr. stderr在这里使用,因为目前stdout已接管的作用stderr的的dummy命令。

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

Now to the outer __2="$( ... )":

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

  • This catches stdoutof the above, which is the stderrof the dummycall, into variable __2. (We could re-use __1here, but I used __2to make it less confusing.). So __2becomes 3b

  • ret="$?";catches the (returned) return code 1(from dummy) again

  • printf '%s=%q\n' "$2" "$__2" >&2;then outputs err=3ato stderr. stderris used again, as it already was used to output the other variable out=2a.

  • printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument tocatch`.

  • 此捕获stdout以上,这是的stderr所述的dummy呼叫,为可变__2。(我们可以__1在这里重复使用,但我曾经__2让它不那么令人困惑。)。于是__2变成3b

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

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

  • printf '( exit %q )' "$ret" >&2; then outputs the code to set the proper return value. I did not find a better way, as assignig it to a variable needs a variable name, which then cannot be used as first oder second argument to赶上`。

Please note that, as an optimization, we could have written those 2 printfas a single one like printf '%s=%q\n( exit %q )"$__2" "$ret"` as well.

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

So what do we have so far?

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

We have following written to stderr:

我们将以下内容写入 stderr:

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

where outis from $1, 2ais from stdoutof dummy, erris from $2, 3bis from stderrof dummy, and the 1is from the return code from dummy.

其中out来自$12a来自stdoutdummyerr来自于$23b来自stderrdummy1来自于返回码来自dummy

Please note that %qin the format of printftakes care for quoting, such that the shell sees proper (single) arguments when it comes to eval. 2aand 3bare so simple, that they are copied literally.

请注意,%q在 的格式中printf注意引用,以便外壳在涉及eval. 2a并且3b非常简单,以至于它们被逐字复制。

Now to the outer eval "$({ ... } 2>&1 )";:

现在到外部eval "$({ ... } 2>&1 )";

This executes all of above which output the 2 variables and the exit, catches it (therefor the 2>&1) and parses it into the current shell using eval.

这将执行以上所有输出 2 个变量和exit,捕获它(因此2>&1)并使用eval.

This way the 2 variables get set and the return code as well.

这样就可以设置 2 个变量和返回码。

Q: It uses evalwhich is evil. So is it safe?

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

  • As long as printf %qhas no bugs, it should be safe. But you always have to be very careful, just think about ShellShock.
  • 只要printf %q没有bug,应该是安全的。但是你总是要非常小心,想想ShellShock。

Q: Bugs?

问:错误?

  • No obvious bugs are known, except following:

    • Catching big output needs big memory and CPU, as everything goes into variables and needs to be back-parsed by the shell. So use it wisely.
    • As usual $(echo $'\n\n\n\n')swallows all linefeeds, not only the last one. This is a POSIX requirement. If you need to get the LFs unharmed, just add some trailing character to the output and remove it afterwards like in following recipe (look at the trailing xwhich allows to read a softlink pointing to a file which ends on a $'\n'):

      target="$(readlink -e "$file")x"
      target="${target%x}"
      
    • Shell-variables cannot carry the byte NUL ($'\0'). They are simply ignores if they happen to occur in stdoutor stderr.

  • The given command runs in a sub-subshell. So it has no access to $PPID, nor can it alter shell variables. You can catcha shell function, even builtins, but those will not be able to alter shell variables (as everything running within $( .. )cannot do this). So if you need to run a function in current shell and catch it's stderr/stdout, you need to do this the usual way with tempfiles. (There are ways to do this such, that interrupting the shell normally does not leave debris behind, but this is complex and deserves it's own answer.)

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

    • 捕获大输出需要大内存和 CPU,因为所有内容都进入变量并需要由 shell 进行反向解析。所以要明智地使用它。
    • 像往常一样$(echo $'\n\n\n\n')吞下所有换行符,而不仅仅是最后一个。这是 POSIX 要求。如果您需要让 LF 不受伤害,只需在输出中添加一些尾随字符,然后像以下配方中那样将其删除(查看尾随x,它允许读取指向以 a 结尾的文件的软链接$'\n'):

      target="$(readlink -e "$file")x"
      target="${target%x}"
      
    • Shell 变量不能携带字节 NUL ( $'\0')。如果它们碰巧发生在stdout或 中,则它们只是被忽略stderr

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

Q: Bash version?

问:Bash 版本?

  • I think you need Bash 4 and above (due to printf %q)
  • 我认为您需要 Bash 4 及更高版本(由于printf %q

Q: This still looks so awkward.

问:这看起来还是很尴尬。

  • Right. Another answer hereshows how it can be done in kshmuch more cleanly. However I am not used to ksh, so I leave it to others to create a similar easy to reuse recipe for ksh.
  • 对。 这里的另一个答案显示了如何ksh更干净地完成它。但是我不习惯ksh,所以我把它留给其他人来创建一个类似的易于重用的食谱ksh

Q: Why not use kshthen?

问:那为什么不使用ksh呢?

  • Because this is a bashsolution
  • 因为这是一个bash解决方案

Q: The script can be improved

问:脚本可以改进

  • Of course you can squeeze out some bytes and create smaller or more incomprehensible solution. Just go for it ;)
  • 当然,您可以挤出一些字节并创建更小或更难以理解的解决方案。就去吧;)

Q: There is a typo. : catch STDOUT STDERR cmd args..shall read # catch STDOUT STDERR cmd args..

问:有错别字。 : catch STDOUT STDERR cmd args..应阅读# catch STDOUT STDERR cmd args..

  • Actually this is intended. :shows up in bash -xwhile comments are silently swallowed. So you can see where the parser is if you happen to have a typo in the function definition. It's an old debugging trick. But beware a bit, you can easily create some neat sideffects within the arguments of :.
  • 其实这是有意为之。 :出现在bash -x评论被默默吞下的时候。因此,如果函数定义中碰巧有拼写错误,您可以看到解析器的位置。这是一个古老的调试技巧。但请注意,您可以轻松地在:.

Edit: Added a couple more ;to make it more easy to create a single-liner out of catch(). And added section how it works.

编辑:添加了更多内容;,以便更轻松地从catch(). 并添加了它是如何工作的部分。

回答by ormaaj

Jonathan has the answer. For reference, this is the ksh93 trick. (requires a non-ancient version).

乔纳森有答案。作为参考,这是 ksh93 技巧。(需要非古代版本)。

function out {
    echo stdout
    echo stderr >&2
}

x=${ { y=$(out); } 2>&1; }
typeset -p x y # Show the values

produces

产生

x=stderr
y=stdout

The ${ cmds;}syntax is just a command substitution that doesn't create a subshell. The commands are executed in the current shell environment. The space at the beginning is important ({is a reserved word).

${ cmds;}语法只是一个命令替换不创建一个子shell。命令在当前 shell 环境中执行。开头的空格很重要({是保留字)。

Stderr of the inner command group is redirected to stdout (so that it applies to the inner substitution). Next, the stdout of outis assigned to y, and the redirected stderr is captured by x, without the usual loss of yto a command substitution's subshell.

内部命令组的 Stderr 被重定向到 stdout(以便它适用于内部替换)。接下来,将 的标准输出out分配给y,重定向的标准错误由 捕获x,而不会丢失y命令替换的子shell。

It isn't possible in other shells, because all constructs which capture output require putting the producer into a subshell, which in this case, would include the assignment.

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

update:Now also supported by mksh.

更新:现在也支持 mksh。

回答by ormaaj

This command sets both stdout (stdval) and stderr (errval) values in the present running shell:

此命令在当前运行的 shell 中设置 stdout (stdval) 和 stderr (errval) 值:

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

provided this function has been defined:

如果已定义此函数:

function setval { printf -v "" "%s" "$(cat)"; declare -p ""; }

Change execcommand to the captured command, be it "ls", "cp", "df", etc.

将 execcommand 更改为捕获的命令,可以是“ls”、“cp”、“df”等。



All this is based on the idea that we could convert all captured values to a text line with the help of the function setval, then setval is used to capture each value in this structure:

所有这一切都是基于这样一个想法,即我们可以在函数 setval 的帮助下将所有捕获的值转换为文本行,然后 setval 用于捕获此结构中的每个值:

execcommand 2> CaptureErr > CaptureOut

Convert each capture value to a setval call:

将每个捕获值转换为 setval 调用:

execcommand 2> >(setval errval) > >(setval stdval)

Wrap everything inside an execute call and echo it:

将所有内容包装在执行调用中并回显它:

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

You will get the declare calls that each setval creates:

您将获得每个 setval 创建的声明调用:

declare -- stdval="I'm std"
declare -- errval="I'm err"

To execute that code (and get the vars set) use eval:

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

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

and finally echo the set vars:

最后回显设置的变量:

echo "std out is : |$stdval| std err is : |$errval|


It is also possible to include the return (exit) value.
A complete bash script example looks like this:

还可以包括返回(退出)值。
一个完整的 bash 脚本示例如下所示:

#!/bin/bash --

# The only function to declare:
function setval { printf -v "" "%s" "$(cat)"; declare -p ""; }

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

回答by Irfy

Technically, named pipes aren't temporary files and nobody here mentions them. They store nothing in the filesystem and you can delete them as soon as you connect them (so you won't ever see them):

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

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

You can have multiple background processes this way and asynchronously collect their stdouts and stderrs at a convenient time, etc.

您可以通过这种方式拥有多个后台进程,并在方便的时间异步收集它们的 stdout 和 stderr,等等。

If you need this for one process only, you may just as well use hardcoded fd numbers like 3 and 4, instead of the {fdout}/{fderr}syntax (which finds a free fd for you).

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

回答by madmurphy

I think before saying “you can't” do something, people should at least give it a try with their own hands…

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

Simple and clean solution, without using evalor anything exotic

简单干净的解决方案,无需使用eval或任何异国情调

1. A minimal version

1. 最小版本

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '
#!/bin/bash
#
# useless.sh
#

echo "This is stderr" 1>&2
echo "This is stdout" 
%s
#!/bin/bash
#
# capture.sh
#

{
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
} < <((printf '
Here is the captured stdout:
This is stdout

And here is the captured stderr:
This is stderr

%s
(printf '
IFS=$'\n' read -r -d '' CAPTURED_STDERR;
%s
IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
' "$(some_command)" 1>&2) 2>&1
' "$(./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
' "$(some_command)" 1>&2) 2>&1)

Requires:printf, read

要求:printf,read

2. A simple test

2. 一个简单的测试

A dummy script for producing stdoutand stderr: useless.sh

用于生成stdout和的虚拟脚本stderruseless.sh

{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
} < <((printf '
{
  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 '
{
  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 '
{
  IFS= read -r -d '' CAPTURED_STDERR;
  IFS= read -r -d '' CAPTURED_EXIT;
  IFS= read -r -d '' CAPTURED_STDOUT;
} < <((printf '
{
  IFS= read -r -d '' CAPTURED_STDERR;
  IFS= read -r -d '' CAPTURED_EXIT;
  IFS= read -r -d '' CAPTURED_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 '
{
    IFS=$'\n' read -r -d '' CAPTURED_STDOUT;
    IFS=$'\n' read -r -d '' CAPTURED_STDERR;
    (IFS=$'\n' read -r -d '' _ERRNO_; exit ${_ERRNO_});
} < <((printf '
# SYNTAX:
#   catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch() {
    {
        IFS=$'\n' read -r -d '' "";
        IFS=$'\n' read -r -d '' "";
        (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
    } < <((printf '
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
%s
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
%d
_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
' "$(((({ ; echo "${?}" 1>&3-; } | tr -d '
_CTRL_Z_=$'\cZ'

# SYNTAX:
#     catch_posix STDOUT_VARIABLE STDERR_VARIABLE COMMAND
catch_posix() {
    {
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "";
        IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" "";
        (IFS=$'\n'"${_CTRL_Z_}" read -r -d "${_CTRL_Z_}" _ERRNO_; return ${_ERRNO_});
    } <<EOF
$((printf "${_CTRL_Z_}%s${_CTRL_Z_}%d${_CTRL_Z_}" "$(((({ ; echo "${?}" 1>&3-; } | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '
# Wrappers to avoid polluting the current shell's environment with variables

: catch_read returncode FD variable
catch_read()
{
eval "=\"\`cat <&\`\"";
# 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 -d '' "";
return ;
}
: catch_1 tempfile variable comand args..
catch_1()
{
{
rm -f "";
"${@:3}" 66<&-;
catch_read $? 66 "";
} 2>&1 >"" 66<"";
}

: catch stdout stderr command args..
catch()
{
catch_1 "`tempfile`" "${2:-stderr}" catch_1 "`tempfile`" "${1:-stdout}" "${@:3}";
}
' 1>&4-) 4>&2- 2>&1- | cut -z -d"${_CTRL_Z_}" -f1 | tr -d '
#######                                                                                                                                                                                                                          
# 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 "" "$c"                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
          done                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
        )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
      )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
    }                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   
    ( (                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 
        "$@" | prefix o >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
        echo  ${PIPESTATUS[0]} | prefix r >&3                                                                                                                                                                                                                                                                                                                                                                                                                                                           
      ) 2>&1 | prefix e >&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
    ) 3>&1                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
  )                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       

extractOne() { # extract                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  echo "" | grep "^" | cut --delimiter=' ' --fields=2 | base64 --decode -                                                                                                                                                                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       
' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) EOF }
' 1>&4-) 4>&2- 2>&1- | tr -d '
GET_STDERR=""
GET_STDOUT=""
get_stderr_stdout() {
    GET_STDERR=""
    GET_STDOUT=""
    unset t_std t_err
    eval "$( (eval ) 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"
' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) }
%s##代码##%d##代码##' "$(((({ some_command; echo "${?}" 1>&3-; } | tr -d '##代码##' 1>&4-) 4>&2- 2>&1- | tr -d '##代码##' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
%s##代码##%d##代码##' "$(some_command)" "${?}" 1>&2) 2>&1)
%s\n##代码##' "$(some_command; printf '##代码##%d' "${?}" 1>&2)" 1>&2) 2>&1)
%s\n##代码##' "$(some_command; printf '##代码##%d' "${?}" 1>&2)" 1>&2) 2>&1)
'; printf '##代码##'; } 2>&1- 1>&4- | tr -d '##代码##' 1>&4-) 3>&1- | xargs printf '##代码##%s##代码##' 1>&4-) 4>&1-)
'; printf '##代码##'; } 2>&1- 1>&4- | tr -d '##代码##' 1>&4-) 3>&1- | xargs printf '##代码##%s##代码##' 1>&4-) 4>&1-)
%s##代码##' "$((some_command | tr -d '##代码##') 3>&1- 1>&2- 2>&3- | tr -d '##代码##')" 1>&2) 2>&1)

The actual script that will capture stdoutand stderr: capture.sh

将捕获stdout和的实际脚本stderrcapture.sh

##代码##

Output of capture.sh

输出 capture.sh

##代码##

3. How it works

3. 工作原理

The command

命令

##代码##

sends the standard output of some_commandto printf '\0%s\0', thus creating the string \0${stdout}\n\0(where \0is a NULbyte and \nis a new line character); the string \0${stdout}\n\0is then redirected to the standard error, where the standard error of some_commandwas already present, thus composing the string ${stderr}\n\0${stdout}\n\0, which is then redirected back to the standard output.

发送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,然后将其重定向回标准输出。

Afterwards, the command

之后,命令

##代码##

starts reading the string ${stderr}\n\0${stdout}\n\0up until the first NULbyte and saves the content into ${CAPTURED_STDERR}. Then the command

开始读取字符串${stderr}\n\0${stdout}\n\0直到第一个NUL字节并将内容保存到${CAPTURED_STDERR}. 然后命令

##代码##

keeps reading the same string up to the next NULbyte and saves the content into ${CAPTURED_STDOUT}.

继续读取相同的字符串直到下一个NUL字节并将内容保存到${CAPTURED_STDOUT}.

4. Making it unbreakable

4. 使其牢不可破

The solution above relies on a NULbyte for the delimiter between stderrand stdout, therefore it will not work if for any reason stderrcontains other NULbytes.

上面的解决方案依赖于一个NUL字节用于之间的分隔符stderrstdout,因此它不会如果由于任何原因的工作stderr包含其他NUL字节。

Although that should never happen, it is possible to make the script completely unbreakable by stripping all possible NULbytes from stdoutand stderrbefore passing both outputs to read(sanitization) – NULbytes would anyway get lost, as it is not possible to store them into shell variables:

尽管这永远不会发生,但可以通过在将两个输出传递给(清理)之前和之前剥离所有可能的NUL字节来使脚本完全牢不可破——字节无论如何都会丢失,因为不可能将它们存储到 shell 变量中stdoutstderrreadNUL

##代码##

Requires:printf, read, tr

需要:printf, read,tr

EDIT

编辑

I have removed one further example for propagating the exit status to the current shell, because, as Andyhas pointed out in the comments, it was not as “unbreakable” as it was supposed to be (since it did not use printfto buffer one of the streams). For the record I paste the problematic code here:

我删除了另一个用于将退出状态传播到当前 shell 的示例,因为正如Andy在评论中指出的那样,它并不像预期的那样“牢不可破”(因为它没有printf用于缓冲其中一个流)。为了记录,我在这里粘贴有问题的代码:

Preserving the exit status (still unbreakable)

The following variant propagates also the exit status of some_commandto the current shell:

##代码##

Requires:printf, read, tr, xargs

保留退出状态(仍然牢不可破)

以下变体还将 的退出状态传播some_command到当前 shell:

##代码##

需要:printf, read, tr,xargs

Andyhas then submitted the following “suggested edit” for capturing the exit code:

然后,安迪提交了以下“建议编辑”以捕获退出代码:

Simple and clean solution saving the exit value

We can add to the end of stderr, a third piece of information, another NULplus the exitstatus of the command. It will be outputted after stderrbut before stdout

##代码##

保存退出值的简单而干净的解决方案

我们可以在 末尾添加stderr第三条信息,另外NUL加上exit命令的状态。它将在之后stderr但之前输出stdout

##代码##

His solution seems to work, but has the minor problem that the exit status should be placed as the last fragment of the string, so that we are able to launch exit "${CAPTURED_EXIT}"within round brackets and not pollute the global scope, as I had tried to do in the removed example. The other problem is that, as the output of his innermost printfgets immediately appended to the stderrof some_command, we can no more sanitize possible NULbytes in stderr, because among these now there is also ourNULdelimiter.

他的解决方案似乎有效,但有一个小问题,即退出状态应作为字符串的最后一个片段放置,以便我们能够exit "${CAPTURED_EXIT}"在圆括号内启动而不污染全局范围,正如我尝试在删除的例子。另一个问题是,由于他最里面的输出printf会立即附加到stderrof 中some_command,我们无法再清理 中可能的NUL字节stderr,因为在这些字节中现在也有我们的NUL分隔符。

After thinking a bit about the ultimate approach, I have come out with a solution that uses printfto cache bothstdoutand the exit code as two different arguments, so that they never interfere.

思考一些关于最终的办法后,我拿出一个解决方案,使用printf缓存stdout和退出代码为两个不同的参数,所以,他们从来没有干涉。

The first thing I did was outlining a way to communicate the exit status to the third argument of printf, and this was something very easy to do in its simplest form (i.e. without sanitization).

我做的第一件事是概述一种将退出状态传达给 的第三个参数的方法printf,这是很容易以最简单的形式(即没有清理)做的事情。

5. Preserving the exit status – the blueprint (without sanitization)

5. 保留退出状态 – 蓝图(未经消毒)

##代码##

Requires:exit, printf, read

需要:exit, printf,read

Things get very messy though when we try to introduce sanitization. Launching trfor sanitizing the streams does in fact overwrite our previous exit status, so apparently the only solution is to redirect the latter to a separate descriptor before it gets lost, keep it there until trdoes its job twice, and then redirect it back to its place.

但是,当我们尝试引入消毒时,事情会变得非常混乱。tr为清理流而启动实际上会覆盖我们之前的退出状态,因此显然唯一的解决方案是在后者丢失之前将其重定向到一个单独的描述符,将其保留在那里直到tr完成它的工作两次,然后将其重定向回它的位置.

After some quite acrobatic redirections between file descriptors, this is what I came out with.

在文件描述符之间进行了一些相当复杂的重定向之后,这就是我得出的结论。

6. Preserving the exit status with sanitization – unbreakable (rewritten)

6. 通过消毒保持退出状态——牢不可破(重写)

The code below is a rewriting of the example that I have removed. It also sanitizes possible NULbytes in the streams, so that readcan always work properly.

下面的代码是对我已删除的示例的重写。它还可以清理NUL流中可能的字节,以便read始终正常工作。

##代码##

Requires:exit, printf, read, tr

需要:exit, printf, read,tr

This solution is really robust. The exit code is always kept separated in a different descriptor until it reaches printfdirectly as a separate argument.

这个解决方案非常强大。退出代码总是在不同的描述符中保持分离,直到它printf作为单独的参数直接到达。

7. The ultimate solution – a general purpose function with exit status

7. 终极解决方案——具有退出状态的通用函数

We can also transform the code above to a general purpose function.

我们还可以将上面的代码转换为通用函数。

##代码##

Requires:cat, exit, printf, read, tr

需要:cat, exit, printf, read,tr

With the catchfunction we can launch the following snippet,

使用该catch函数,我们可以启动以下代码段,

##代码##

and get the following result:

并得到以下结果:

##代码##

8. What happens in the last examples

8. 在最后的例子中发生了什么

Here follows a fast schematization:

下面是一个快速的示意图:

  1. some_commandis launched: we then have some_command's stdouton the descriptor 1, some_command's stderron the descriptor 2 and some_command's exit code redirected to the descriptor 3
  2. stdoutis piped to tr(sanitization)
  3. stderris swapped with stdout(using temporarily the descriptor 4) and piped to tr(sanitization)
  4. the exit code (descriptor 3) is swapped with stderr(now descriptor 1) and piped to exit $(cat)
  5. stderr(now descriptor 3) is redirected to the descriptor 1, end expanded as the second argument of printf
  6. the exit code of exit $(cat)is captured by the third argument of printf
  7. the output of printfis redirected to the descriptor 2, where stdoutwas already present
  8. the concatenation of stdoutand the output of printfis piped to read
  1. some_command启动:我们再有some_command'Sstdout的描述符1some_commandstderr对描述符2和some_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. The POSIX-compliant version #1 (breakable)

9. 符合 POSIX 的版本 #1(可破解)

Process substitutions(the < <()syntax) are not POSIX-standard (although they de factoare). In a shell that does not support the < <()syntax the only way to reach the same result is via the <<EOF … EOFsyntax. Unfortunately this does not allow us to use NULbytes as delimiters, because these get automatically stripped out before reaching read. We must use a different delimiter. The natural choice falls onto the CTRL+Zcharacter (ASCII character no. 26). Here is a breakableversion (outputs must never contain the CTRL+Zcharacter, or otherwise they will get mixed).

进程替换< <()语法)不是 POSIX 标准的(尽管它们事实上是)。在不支持< <()语法的shell 中,获得相同结果的唯一方法是通过<<EOF … EOF语法。不幸的是,这不允许我们使用NUL字节作为分隔符,因为它们在到达read. 我们必须使用不同的分隔符。自然选择落在CTRL+Z字符上(ASCII 字符编号 26)。这是一个可破解的版本(输出不得包含该CTRL+Z字符,否则它们会混在一起)。

##代码##

Requires:exit, printf, read

需要:exit, printf,read

10. The POSIX-compliant version #2 (unbreakable, but not as good as the non-POSIX one)

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

And here is its unbreakable version, directly in function form (if either stdoutor stderrcontain CTRL+Zcharacters, the stream will be truncated, but will never be exchanged with another descriptor).

这是它的牢不可破的版本,直接以函数形式(如果其中一个stdoutstderr包含CTRL+Z字符,则流将被截断,但永远不会与另一个描述符交换)。

##代码##

Requires:cat, cut, exit, printf, read, tr

需要:cat, cut, exit, printf, read,tr

回答by Tino

For the benefit of the reader here is a solution using tempfiles.

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

The question was not to use tempfiles. However this might be due to the unwanted pollution of /tmp/with tempfile in case the shell dies. In case of kill -9some trap 'rm "$tmpfile1" "$tmpfile2"' 0does not fire.

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

If you are in a situation where you can use tempfile, but want to never leave debris behind, here is a recipe.

如果您处于可以使用tempfile但不想留下碎片的情况下,这里有一个秘诀。

Again it is called catch()(as my other answer) and has the same calling syntax:

它再次被调用catch()(作为我的另一个答案)并且具有相同的调用语法:

catch stdout stderr command args..

catch stdout stderr command args..

##代码##

What it does:

它能做什么:

  • It creates two tempfiles for stdoutand stderr. However it nearly immediately removes these, such that they are only around for a very short time.

  • catch_1()catches stdout(FD 1) into a variable and moves stderrto stdout, such that the next ("left") catch_1can catch that.

  • Processing in catchis done from right to left, so the left catch_1is executed last and catches stderr.

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

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

  • 处理 incatch从右到左完成,所以 leftcatch_1最后执行并捕获stderr

The worst which can happen is, that some temporary files show up on /tmp/, but they are always empty in that case. (They are removed before they get filled.). Usually this should not be a problem, as under Linux tmpfs supports roughly 128K files per GB of main memory.

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

  • The given command can access and alter all local shell variables as well. So you can call a shell function which has sideffects!

  • This only forks twice for the tempfilecall.

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

  • 这只会为tempfile呼叫分叉两次。

Bugs:

错误:

  • Missing good error handling in case tempfilefails.

  • This does the usual \nremoval of the shell. See comment in catch_read().

  • You cannot use file descriptor 66to pipe data to your command. If you need that, use another descriptor for the redirection, like 42(note that very old shells only offer FDs up to 9).

  • This cannot handle NUL bytes ($'\0') in stdoutand stderr. (NUL is just ignored. For the readvariant everything behind a NUL is ignored.)

  • 如果tempfile失败,则缺少良好的错误处理。

  • 这将执行通常\n的外壳移除。见评论catch_read()

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

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

FYI:

供参考:

  • Unix allows us to access deleted files, as long as you keep some reference to them around (such as an open filehandle). This way we can open and then remove them.
  • Unix 允许我们访问已删除的文件,只要您保留对它们的一些引用(例如打开的文件句柄)。这样我们就可以打开然后删除它们。

回答by mncl

Did not like the eval, so here is a solution that uses some redirection tricks to capture program output to a variable and then parses that variable to extract the different components. The -w flag sets the chunk size and influences the ordering of std-out/err messages in the intermediate format. 1 gives potentially high resolution at the cost of overhead.

不喜欢 eval,所以这里有一个解决方案,它使用一些重定向技巧将程序输出捕获到变量,然后解析该变量以提取不同的组件。-w 标志设置块大小并影响中间格式中 std-out/err 消息的顺序。1 以开销为代价提供了潜在的高分辨率。

##代码##

回答by Eduardo Lucio

What about... =D

怎么样... =D

##代码##

回答by Jonathan Leffler

Succinctly, I believe the answer is 'No'. The capturing $( ... )only captures standard output to the variable; there isn't a way to get the standard error captured into a separate variable. So, what you have is about as neat as it gets.

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