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

时间:2012-06-14 06:19:08

标签: bash shell command-line io-redirection

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

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

16 个答案:

答案 0 :(得分:34)

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

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

答案 1 :(得分:15)

乔纳森有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的标准输出分配给y,重定向的stderr由x捕获,而不会通常丢失y到命令替换的子shell。

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

更新:现在也受到mksh支持。

答案 2 :(得分:14)

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

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

如果已定义此功能:

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

将execcommand更改为捕获的命令,无论是&#34; ls&#34;,&#34; cp&#34;,&#34; df&#34;等等。


所有这些都是基于我们可以借助函数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) )"

最后回应set vars:

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

答案 3 :(得分:11)

为了读者的利益,sum所有up {/ 3}},这是一个

Easy Reusable bash解决方案

此版本使用子shell并在没有tempfile的情况下运行。 (对于没有子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。真的没什么复杂的。

讨论

问:它是如何运作的?

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

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

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

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

  • __1="$("$("${@:3}")"; } 2>&1;执行dummy 1 2 3并将其stdout存储到__1中以供日后使用。因此__1变为2a。它还会将stderr的{​​{1}}重定向到dummy,以便外部捕获可以收集stdout

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

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

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

现在到外1

  • 这会将上述__2="$( ... )" stdout调用的stderr捕获到变量dummy中。 (我们可以在这里重复使用__2,但我使用__1来减少混淆。)因此__2变为__2

  • 3b再次捕获(返回)返回代码ret="$?";(来自1

  • dummy然后将printf '%s=%q\n' "$2" "$__2" >&2;输出到err=3a。再次使用stderr,因为它已用于输出其他变量stderr

  • out=2a catch`。

请注意,作为优化,我们可以将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作为单个printf&#34; $ __ 2&#34; &#34; $ ret&#34;`以及。

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

我们写了以下内容给stderr:

printf '%s=%q\n( exit %q )

其中out=2a err=3b ( exit 1 ) 来自out$1来自2a stdoutdummy来自err,{ {1}}来自$2 3b,而stderr来自dummy的返回代码。

请注意1格式dummy负责引用,以便shell在%q时看到正确的(单个)参数。 printfeval非常简单,可以按字面意思复制。

现在到外2a

执行以上所有操作,输出2个变量和3b,捕获它(因此eval "$({ ... } 2>&1 )";)并使用exit将其解析为当前shell。

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

问:它使用的是2>&1,这是邪恶的。这样安全吗?

  • 只要eval没有错误,就应该是安全的。但是你总是要非常小心,只要想想ShellShock。

问:错误?

  • 除了以下内容外,没有明显的错误:

    • 捕获大量输出需要大内存和CPU,因为一切都变成了变量,需要由shell进行反向解析。所以明智地使用它。
    • 像往常一样eval 吞下所有换行,而不仅仅是最后一行。这是POSIX要求。如果你需要保持LF不受伤害,只需在输出中添加一些尾随字符,然后将其删除,如下面的配方(查看尾随printf %q,它允许读取指向以{结尾的文件的软链接} {1}}):

      $(echo $'\n\n\n\n')
    • Shell变量不能携带字节NUL(x)。如果它们碰巧发生在$'\n'target="$(readlink -e "$file")x" target="${target%x}" 中,它们就会被忽略。

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

问:Bash版本?

  • 我认为您需要Bash 4及以上版本(由于catch
问:这仍然看起来很尴尬。

  • 右键。 Another answer here显示了如何更加干净地在$( .. )中完成它。但是我不习惯tempfile,所以我将其留给其他人为printf %q创建一个类似的易于重复使用的食谱。

问:为什么不使用ksh

  • 因为这是ksh解决方案

问:脚本可以改进

  • 当然,您可以挤出一些字节并创建更小或更难以理解的解决方案。只是去吧;)
问:有一个错字。 ksh应为ksh

  • 实际上这是有意的。 bash : catch STDOUT STDERR cmd args..会在# catch STDOUT STDERR cmd args..显示,而评论会被默默吞噬。因此,如果碰巧在函数定义中出现拼写错误,您可以看到解析器的位置。这是一个旧的调试技巧。但要注意一点,你可以在:的参数中轻松创建一些干净的副作用。

修改:添加了多个bash -x,以便更轻松地从:创建单线程。并添加了它的工作原理。

答案 4 :(得分:7)

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

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

您可以通过这种方式拥有多个后台进程,并在方便的时候异步收集他们的stdouts和stderrs等。

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

答案 5 :(得分:5)

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

简单干净的解决方案,无需使用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)

要求printfread

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

随后,该命令

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包含其他{{1 }}个字节。

尽管永远不会发生,但可以通过将两个输出都传递到NUL之前从NULstdout剥离所有可能的stderr个字节来使脚本完全不可破坏(消毒)– read个字节总会丢失,就像it is not possible to store them into shell variables

NUL

要求{ IFS=$'\n' read -r -d '' CAPTURED_STDOUT; IFS=$'\n' read -r -d '' CAPTURED_STDERR; } < <((printf '\0%s\0' "$((some_command | tr -d '\0') 3>&1- 1>&2- 2>&3- | tr -d '\0')" 1>&2) 2>&1) printfread

编辑

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

保留退出状态(仍不可中断)

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

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

Andy 随后提交了以下“建议的编辑”以捕获退出代码:

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

我们可以在xargs的末尾添加第三条信息,另一条stderr和命令的NUL状态。将在exit之后但在stderr

之前输出
stdout

他的解决方案似乎有效,但是有一个小问题,即退出状态应放置在字符串的最后一个片段中,以便我们能够在圆括号内启动{ IFS= read -r -d '' CAPTURED_STDERR; IFS= read -r -d '' CAPTURED_EXIT; IFS= read -r -d '' CAPTURED_STDOUT; } < <((printf '\0%s\n\0' "$(some_command; printf '\0%d' "${?}" 1>&2)" 1>&2) 2>&1) 而不污染全局范围,就像我在删除的示例中尝试做的那样。另一个问题是,随着他最里面的exit "${CAPTURED_EXIT}"的输出被立即附加到printf的{​​{1}}上,我们无法再清理{{1 }},因为其中还有 our stderr分隔符。

在仔细考虑了最终方法之后,我提出了一个解决方案,该解决方案使用some_command来缓存两者 NUL和退出代码作为两个不同的参数,这样他们就不会干涉。

我所做的第一件事是概述一种将退出状态传达给stderr的第三个参数的方法,这是最简单的形式(即无需消毒),很容易做到。

5。保留退出状态–蓝图(无需消毒)

NUL

要求printfstdoutprintf

当我们尝试引入消毒时,事情变得非常混乱。实际上,启动{ 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继续工作之前两次,然后将其重定向回原位。

在文件描述符之间进行了一些非常杂技的重定向之后,这就是我的想法。

6。保持出口状态并进行消毒–牢不可破(重写)

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

tr

要求trNULread{ 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

7。最终解决方案–具有退出状态的通用功能

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

printf

要求readtrprintf# 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

8。最后一个例子发生了什么

这里是快速的模式化:

  1. tr被启动:然后在描述符1上有catch的{​​{1}}在描述符2上有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 的{​​{1}}在{2 1}}的退出代码重定向到描述符3
  2. 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 通过管道传输到some_command(消毒)
  3. some_commandstdout交换(临时使用描述符4)并通过管道传递到some_command(消毒)
  4. 将退出代码(描述符3)与stderr(现在为描述符1)交换,并通过管道传递到some_command
  5. stdout(现为描述符3)重定向到描述符1,并以tr的第二个自变量扩展
  6. stderr的退出代码由stdout的第三个参数捕获
  7. tr的输出重定向到描述符2,其中stderr已经存在
  8. exit $(cat)的串联和stderr的输出通过管道传递到printf

9。符合POSIX的版本#1(易碎)

Process substitutionsexit $(cat)语法)不是POSIX标准的(尽管它们实际上是 )。在不支持printf语法的shell中,达到相同结果的唯一方法是通过printf语法。不幸的是,这不允许我们使用stdout个字节作为分隔符,因为这些字节在到达stdout之前会被自动剥离。我们必须使用其他定界符。自然选择落在printf字符(ASCII字符编号26)上。这是一个易碎版本(输出不得包含read字符,否则它们将变得混乱)。

< <()

要求< <()<<EOF … EOFNUL

10。兼容POSIX的版本#2(牢不可破,但不如非POSIX版本好)

这是它的坚不可摧的版本,直接以函数形式显示(如果readCTRL+Z包含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

要求exitprintfreadstdoutstderrCTRL+Z

答案 6 :(得分:3)

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

答案 7 :(得分:2)

简洁地说,我相信答案是'不'。捕获$( ... )仅捕获变量的标准输出;没有办法将标准错误捕获到单独的变量中。所以,你所拥有的就像它得到的一样整洁。

答案 8 :(得分:2)

怎么样...... = 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"

答案 9 :(得分:2)

为了读者的利益,这里有一个使用tempfile s。

的解决方案

问题不是使用tempfile。然而,这可能是由于在shell死亡的情况下使用临时文件对/tmp/进行了不必要的污染。在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}";
}

它的作用:

  • 它会为tempfilestdout创建两个stderr。然而,它几乎立即消除了这些,因此它们只是在很短的时间内。

  • catch_1()stdout(FD 1)捕获到变量中并将stderr移至stdout,以便下一个(&#34; left&#34} ;)catch_1可以捕获它。

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

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

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

  • 这只会在tempfile电话中分叉两次。

错误:

  • tempfile失败时缺少良好的错误处理。

  • 通常\n删除shell。请参阅catch_read()中的评论。

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

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

供参考:

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

答案 10 :(得分:0)

如果命令1)没有状态副作用,2)计算上便宜,最简单的解决方案就是运行两次。我主要将此用于在启动过程中运行的代码,当您还不知道磁盘是否正常工作时。在我的情况下它是一个很小的some_command所以没有性能命中运行两次,并且该命令没有副作用。

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

示例:

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调用。

答案 11 :(得分:0)

这是一个更简单的变体,并不是OP想要的,但不同于任何其他选项。您可以通过重新排列文件描述符来获得所需的任何内容。

测试命令:

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

本身就是:

%> ./xx.sh
stdout
stderr

现在,打印stdout,将stderr捕获到变量,&amp;将stdout记录到文件

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

或记录标准输出&amp;将stderr捕获到变量:

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

你明白了。

答案 12 :(得分:0)

一种解决方法是hacky,但可能比本页面上的一些建议更直观,是标记输出流,合并它们,然后根据标记进行拆分。例如,我们可以使用&#34; STDOUT&#34;标记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是受限制的表单,您可以提出一个与其允许的内容不冲突的标记。

答案 13 :(得分:0)

警告:不(但是?)正在工作!

以下似乎可能导致它在没有创建任何临时文件的情况下工作,并且仅在POSIX sh上工作;然而,它需要base64,并且由于编码/解码可能效率不高并且也使用&#34;更大&#34;存储器中。

  • 即使在简单的情况下,当最后一个stderr行没有换行符时,它也会失败。至少在某些情况下,可以用&#34; {exe;替换exe来修复这个问题。 echo&gt;&amp; 2; }&#34;,即添加换行符。
  • 然而,主要的问题是一切似乎都很活泼。尝试使用像:

    这样的exe

    EXE() {     cat /usr/share/hunspell/de_DE.dic     cat /usr/share/hunspell/en_GB.dic&gt;&amp; 2 }

你会看到,例如base64编码行的部分位于文件的顶部,部分位于末尾,而未解码的stderr位于中间。

好吧,即使下面的想法不能发挥作用(我假设),它也可能成为那些可能错误地相信它可以像这样工作的人的反例。 < / p>

想法(或反例):

#!/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 :(得分:0)

这是@madmurphy解决方案的一种变体,适用于任意大的stdout / stderr流,维护出口返回值并处理流中的null(通过将它们转换为换行符)

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命令是该操作中最昂贵的部分。例如:在运行500个进程的计算机上,find /proc花费20秒(而命令仅为0.5秒)。第一次阅读需要10秒,第二次阅读需要10秒,使总时间增加了一倍。

解释缓冲区

最初的解决方案是使用printf的参数来缓冲流,但是由于需要使退出代码排在最后,因此一种解决方案是同时缓冲stdout和stderr。我尝试了xargs -0 printf,但随后您很快就达到了“最大参数长度限制”。因此,我决定一个解决方案是编写一个快速缓冲功能:

  1. 使用read将流存储在变量中
  2. read将在流结束或接收到null时终止。由于我们已经删除了null,因此它在流关闭时结束,并返回非零值。由于这是预期的行为,因此我们添加|| :表示“或true”,以便该行始终求值为true(0)
  3. 现在我知道流已经结束,可以安全地开始将其回显。
  4. echo -n "${buf}"是内置命令,因此不限于参数长度限制
  5. 最后,在结尾处添加一个空分隔符。

答案 15 :(得分:0)

实时输出并写入文件:

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