保存标准输出、标准错误和标准输出+标准错误的同步方法

10
为了测试目的,我想要将stdout和stderr分别保存下来以便后续代码检查。例如,使用错误输入运行测试应该会导致stderr输出,但是stdout没有任何输出;而使用正确输入运行测试应该会导致stdout输出,但是不会有stderr输出。保存必须是同步的,以避免与测试发生竞争条件(因此无法使用进程替换)。
为了能够事后调试测试,我还需要按它们输出的顺序看到stdout和stderr。所以,我要么将它们都保存到同一个文件/变量/其他地方,要么在同时将它们保存分开时也要将它们发送到终端。
为了测试哪个错误发生了,我还需要命令的退出码。
出于效率和准确性的原因,我当然不能两次运行每个测试。
例如,是否可以将stdout重定向到stdout.log,stderr重定向到stderr.log,并且将它们同时保存到output.log中?还是分别为stdout和stderr使用一个同步的tee命令?或者将一份stdout和stderr的副本保存到不同的变量中? 更新:看起来tim的解决方案几乎可行(修改为在终端上输出而不是记录到all.log):
$ set -o pipefail
$ {
    {
        echo foo | tee stdout.log 2>&3 3>&-
    } 2>&1 >&4 4>&- | tee stderr.log 2>&3 3>&-
} 3>&2 4>&1
foo
$ cat stdout.log
foo
$ cat stderr.log
$ {
    {
        echo foo >&2 | tee stdout.log 2>&3 3>&-
    } 2>&1 >&4 4>&- | tee stderr.log 2>&3 3>&-
} 3>&2 4>&1
foo
$ cat stdout.log
$ cat stderr.log
foo
$ bar=$({
    {
        echo foo | tee stdout.log 2>&3 3>&-
    } 2>&1 >&4 4>&- | tee stderr.log 2>&3 3>&-
} 3>&2 4>&1)
$ echo "$bar"
foo
$ cat stdout.log
foo
$ cat stderr.log
$ bar=$({
    {
        echo foo >&2 | tee stdout.log 2>&3 3>&-
    } 2>&1 >&4 4>&- | tee stderr.log 2>&3 3>&-
} 3>&2 4>&1)
$ cat stdout.log
$ cat stderr.log
foo
$ echo "$bar"
foo

这似乎是有效的,除了最后一次迭代,bar 的值被设置为 stderr 的内容。有没有建议让所有这些都能正常工作?

3个回答

11
请参考BashFAQ/106。在记录标准输出(stdout)和标准错误(stderr)到同一文件的同时,保持它们原本的独立目标是很困难的。因此,我们需要使用两个单独的进程来写入stdout和stderr,并将它们都以追加模式打开。只有以这种方式打开才能确保数据不会相互覆盖。注意,在shell脚本中无法编写读取两个FD并在其中一个有输入时轮流出现的过程,因为shell没有poll(2)或select(2)接口。
# Bash
> mylog
exec > >(tee -a mylog) 2> >(tee -a mylog >&2)

echo A >&2
cat file
echo B >&2

这可以确保日志文件的正确性,但不能保证写入操作完成后立即出现下一个 shell 提示符:

~$ ./foo
A
hi mom
B
~$ cat mylog
A
hi mom
B
~$ ./foo
A
hi mom
~$ B

另请参见BashFAQ/002BashFAQ/047


1
假设我们正在讨论基于行的字符串,我们是否可以将两个流通过sed管道传递,以在行开头添加标记?然后,我们就能够从日志文件中知道哪一行来自哪个流。 - Raphael
1
@Raphael:你应该能够在每个进程替代中使用tee,例如:exec > >(sed 's/^/from STDOUT: /' | tee -a mylog) 2> >(sed 's/^/from STDERR: /' | tee -a mylog >&2)(未经测试)。顺便说一下,这会导致终端输出也具有这些前缀。 - Dennis Williamson
太酷了!这不是让你引用的部分失效了吗?现在我们有一个同步的共享输出文件,仍然知道哪个是哪个,或者我漏掉了什么? - Raphael
@Raphael:抱歉,我不明白哪里被否决了。 - Dennis Williamson
也许我误解了第一段,认为这是不可能的。 - Raphael

2
这听起来像是 multitee 的任务。
如果你想要测试同步输出或磁盘写入而不使用进程替换,你可能需要尝试一些 Bash shell 重定向技巧和 tee(1) 命令,类似于以下示例:
echos() { echo "hello on stdout"; echo "hello on stderr" 1>&2; return 0; }

   { 
  { 
     echos 3>&- | 
        tee stdout.log 2>&3 3>&- 
  } 2>&1 >&4 4>&- | 
        tee  stderr.log 2>&3 3>&- 
} 3>&2 4>&1 2>&1 | tee /dev/stderr > all.log

open -e stdout.log stderr.log all.log   # Mac OS X

更多信息请参见:http://codesnippets.joyent.com/posts/show/8769

要获取命令的退出码,您可能需要分别查看pipefail或PIPESTATUS(除了最后一个退出码变量"$?"之外):

help set | grep -i pipefail
man bash | less -p pipefail
man bash | less -p PIPESTATUS

PIPESTATUS似乎无法工作 - 如果echos命令返回1,它仍然只是两个零。 - l0b0
这里有一个不错的“重定向演示表”,链接为https://mywiki.wooledge.org/BashFAQ/002(往下滚动),可能会帮助人们尝试弄清楚重定向是如何工作的。 - Aeronautix
这里有一个“多重重定向”示例的详细说明,供感兴趣的人们参考:https://wiki.bash-hackers.org/howto/redirection_tutorial#an_example - Aeronautix

1
在最后一次迭代中,当 bar 的值设置为 stderr 的内容时,请尝试:
# restore original stdout & stderr at the end by adding: 1>&2 2>&1
bar="$(
{
   {
      echo foo >&2 | tee stdout.log 2>&3 3>&-
   } 2>&1 >&4 4>&- | tee stderr.log 2>&3 3>&-
} 3>&2 4>&1 1>&2 2>&1
)"

echo "$bar"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接