在不同的variables(bash)中存储/捕获stdout和stderr

是否有可能存储或捕获stdout和stderr在不同的variables ,而不使用临时文件? 现在我这样做,在运行some_command时得到标准out和错误err ,但我想避免临时文件。

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

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

 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)需要被实际的命令replace。 标准输出保存到数组$t_std行一行,省略换行符( -t )和标准错误代码 $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 ,版本4.2.37(1) – 发行(i486-pc-linux-gnu)testingDebian wheezy。

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

 function out { echo stdout echo stderr >&2 } x=${ { y=$(out); } 2>&1; } typeset -pxy # Show the values 

产生

 x=stderr y=stdout 

${ cmds;}语法只是一个不会创build子shell的命令replace。 这些命令在当前的shell环境中执行。 开始的空间很重要( {是一个保留字)。

内部命令组的stderr被redirect到stdout(所以它适用于内部replace)。 接下来, out的stdout被赋值给y ,并且被redirect的stderr被x捕获,而没有通常的y丢失到一个命令replace的子shell。

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

更新:现在也支持mksh。

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

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

提供了这个function已被定义:

 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创build的声明调用:

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

要执行该代码(并获取variables集)使用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|" 

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

易于重用的bash解决scheme

这个版本使用子shell并且运行时没有tempfile 。 (对于没有子壳的tempfile版本,请参阅我的其他答案 。)

 : 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..前面command args..你就完成了。

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

讨论

问:它是如何工作的?

它只是将来自其他答案的想法包含在一个函数中,以便它可以很容易地被重用。

catch()基本上使用eval来设置这两个variables。 这与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 ,同时redirectdummy stderr stdout ,使外部捕获可以收集stdout

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

  • printf '%q=%q\n' "$1" "$__1" >&2; 然后输出out=2astderr 。 这里使用的是stderr ,因为当前stdout已经接pipe了dummy命令的stderrangular色。

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

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

  • 这将上面的stdout捕获到variables__2 ,这是dummy调用的stderr 。 (我们可以在这里重新使用__1 ,但是我用__2来减less混淆。) 所以__2变成3b

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

  • printf '%s=%q\n' "$2" "$__2" >&2; 然后输出err=3astderrstderr被再次使用,因为它已经被用来输出另一个variablesout=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 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两个printf写成一个printf '%s=%q\n( exit %q ) “$ __ 2”“$ ret”“。

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

我们已经写下了stderr:

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

$12adummy stdouterr$23bdummy stderr开始,而1则从dummy的返回代码开始。

请注意, printf格式的%q需要引用,这样shell在eval时会看到正确的(单个)参数。 2a3b非常简单,它们被复制。

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

这将执行以上输出2个variables和exit ,捕获它(为此, 2>&1 )并使用eval将其parsing到当前shell中。

这样,2个variables得到设置和返回码。

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

  • 只要printf %q没有错误,就应该是安全的。 但是你一定要非常小心,只要想想ShellShock。

问:错误?

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

    • 捕获大的输出需要大的内存和CPU,因为一切都进入variables,需要被shellparsing。 所以明智地使用它。
    • 和往常一样, $(echo $'\n\n\n\n') 吞下所有换行符 ,而不仅仅是最后一个换行符 。 这是POSIX的要求。 如果你需要让LF不受伤害,只需要在输出中添加一些尾随的字符,然后像下面的配方一样去除它(看看尾部的x ,它允许读取一个指向一个$'\n'结尾的文件的软链接):

       target="$(readlink -e "$file")x" target="${target%x}" 
    • Shellvariables不能携带字节NUL( $'\0' )。 如果它们发生在stdoutstderr它们就会被忽略。

  • 给定的命令在子shell中运行。 所以它不能访问$PPID ,也不能改变shellvariables。 你可以catch一个shell函数,甚至是builtins,但是那些将不能改变shellvariables(因为在$( .. )运行的所有东西都不能这样做)。 所以,如果你需要在当前shell中运行一个函数,并捕获它的stderr / stdout,你需要用tempfile通常的方式来完成。 (有办法可以这样做,打断shell通常不会留下碎片,但这是复杂的,值得自己回答。)

问:Bash版本?

  • 我认为你需要Bash 4以上(由于printf %q

问:这仍然看起来很尴尬。

  • 对。 这里的另一个答案显示了如何在ksh做得更干净。 不过我不习惯ksh ,所以我把它留给别人去创build一个类似的轻松重用ksh配方。

问:为什么不使用ksh呢?

  • 因为这是一个bash解决scheme

问:脚本可以改进

  • 当然,你可以挤出一些字节,创build更小或更难以理解的解决scheme。 只要去它;)

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

  • 其实这是有意的。 :bash -x显示,而注释被无声地吞噬。 因此,如果在函数定义中碰巧有拼写错误,则可以看到parsing器在哪里。 这是一个古老的debugging技巧。 但是要小心一点,你可以在下面的参数中轻松地创造一些干净的副作用:

编辑:增加了一些; 以便更容易地创build一个单一的catch() 。 并添加部分如何工作。

从技术上讲,命名pipe道不是临时文件,没有人在这里提到它们。 他们没有在文件系统中存储任何东西,你可以删除它们,只要你连接它们(所以你永远不会看到它们):

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

您可以通过这种方式获得多个后台进程,并在方便的时候asynchronous收集stdout和stderrs等。

如果你仅仅需要一个进程,你可以使用硬编码的fd数字,比如3和4,而不是{fdout}/{fderr}语法(为你find一个自由的fd)。

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

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

为了读者的利益,这里是一个使用tempfile的解决scheme。

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

如果你处于可以使用tempfile ,但是不想留下碎片 ,这里是一个配方。

再次,它被称为catch() (作为我的其他答案 ),并具有相同的调用语法:

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创build两个tempfile 。 然而,它几乎立即消除这些,使他们只在很短的时间内。

  • catch_1() stdout (FD 1 catch_1()捕获到一个variables中,并将stderr移动到stdout ,这样下一个(“左”) catch_1就可以捕获该数据。

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

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

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

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

错误:

  • tempfile失败的情况下缺less良好的error handling。

  • 这样做通常\n删除壳。 请参阅catch_read()注释。

  • 您不能使用文件描述符66将数据传输到您的命令。 如果你需要的话,使用另一个描述符作为redirect,比如42 (注意,非常旧的shell只能提供高达9的FD)。

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

供参考:

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

如果命令1)没有有状态的副作用和2)在计算上便宜,最简单的解决方法是运行两次。 我主要使用这个代码在引导序列中运行,当你还不知道磁盘是否正在工作。 在我的情况下,这是一个微小的some_command所以没有两次运行的性能,命令没有副作用。

主要好处是这是干净的,易于阅读。 这里的解决scheme非常聪明,但我不愿意成为一个包含更复杂解决scheme的脚本。 如果你的scheme适用于这种情况,我build议使用简单的两次运行方法,因为它更简洁,更易于维护。

例:

 output=$(getopt -o '' -l test: -- "$@") errout=$(getopt -o '' -l test: -- "$@" 2>&1 >/dev/null) if [[ -n "$errout" ]]; then echo "Option Error: $errout" fi 

再次,这只是确定的,因为getopt没有副作用。 我知道这是性能安全的,因为我的父母代码在整个程序中调用次数less于100次,用户将永远不会注意到100个getopt调用和200个getopt调用。

这是一个简单的变化,不是OP想要的,但是不像其他任何选项。 你可以通过重新排列文件描述符来得到你想要的。

testing命令:

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

这本身确实:

 %> ./xx.sh stdout stderr 

现在,打印stdout,将stderr捕获到一个variables,&log stdout到一个文件

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

或者将stdout和stderrlogging到一个variables中:

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

你明白了。

一种变通方法,比起这个页面上的一些build议,可能比较直观,但是可以标记输出stream,合并它们,然后根据标签进行拆分。 例如,我们可以用“STDOUT”前缀标记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是受限制的表单,则可以使用与其允许的内容不冲突的标签。

警告:不(?)工作!

以下似乎有可能导致它没有创build任何临时文件,也仅在POSIX sh上工作; 它需要base64然而,由于编码/解码可能不那么高效,并使用“更大”的内存。

  • 即使在简单的情况下,当最后一个stderr行没有换行时,它也会失败。 至less在某些情况下,可以用“{exe; echo>&2;}”来代替exe,即添加换行符。
  • 主要的问题是,一切似乎活泼。 尝试使用一个exe文件,如:

    exe(){cat /usr/share/hunspell/de_DE.dic cat /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 

(至less在Debian的破折号和bash上)