在不同的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=2a
到stderr
。 这里使用的是stderr
,因为当前stdout
已经接pipe了dummy
命令的stderr
angular色。 -
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=3a
到stderr
。stderr
被再次使用,因为它已经被用来输出另一个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 )
从$1
, 2a
从dummy
stdout
, err
从$2
, 3b
从dummy
stderr
开始,而1
则从dummy
的返回代码开始。
请注意, printf
格式的%q
需要引用,这样shell在eval
时会看到正确的(单个)参数。 2a
和3b
非常简单,它们被复制。
现在到外面评价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'
)。 如果它们发生在stdout
或stderr
它们就会被忽略。
-
给定的命令在子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}"; }
它能做什么:
-
它为
stdout
和stderr
创build两个tempfile
。 然而,它几乎立即消除这些,使他们只在很短的时间内。 -
catch_1()
stdout
(FD 1catch_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)。 -
这不能处理
stdout
和stderr
NUL字节($'\0'
)。 (NUL被忽略,对于read
variables,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上)