如何将标准输出和标准错误输出重定向到一个文件,同时保持流分离?

19

将标准输出和标准错误重定向到一个文件,同时仍将其输出到标准输出是很简单的:

cmd 2>&1 | tee output_file

但是现在cmd的标准输出(stdout)和错误输出(stderr)都显示在stdout上。我想把stdout和stderr写入同一个文件中(假设cmd是单线程,那么顺序得以保留),但仍然希望能够分别重定向它们,类似于这样:

some_magic_tee_variant combined_output cmd > >(command-expecting-stdout) 2> >(command-expecting-stderr)

所以,combined_output 包含了两者的内容,并且保留了顺序,但是期望命令输出stdout 的命令只会得到stdout,期望命令输出stderr 的命令只会得到stderr。基本上,我想记录stdout+stderr,同时允许分别重定向和管道stdout和stderr。 tee 方法的问题在于它将它们聚合在一起。有没有办法在 bash/zsh 中实现这一点?


4
看起来这不是一个 shell 的问题,而是一个 *nix 问题:没有办法将文件描述符“拆分”为指向两个不同的事物,因此可靠地保留标准输出和标准错误输出的顺序的唯一方法是复制“原始”的文件描述符,使它们指向相同的位置 - 在此之后,你就无法再区分它们了,因为它们实际上是完全相同的。 - ruakh
1
我认为你可以用C语言编写一些魔法Tee变体。它将轮询stdout/stderr,当它在任一流上接收到数据时,会立即将其写入文件,然后在相应的流上输出。从技术上讲,如果调度程序将魔法Tee变体置于睡眠状态,然后它从轮询中唤醒并等待stdout和stderr上的数据,那么它可能无法完全保留顺序--但是我想即使是shell将stdout和stderr都输出到tty上也存在重新排序的情况?这可能是我要发布的一个很好的单独问题... - Joseph Garvin
2
关于“我想即使shell将stdout和stderr输出到tty,重新排序也存在”的问题:我不确定POSIX/SUS等允许什么行为,但在正常实现中,没有重新排序。其工作方式是,shell只是将文件描述符1和2(stdout和stderr)指向例如/dev/tty17,然后运行命令。这并不是说shell必须轮询命令的输出并将该输出转发到TTY。 - ruakh
有趣的需求。您是否打算在合并文件中的每一行前缀为“1:”以表示标准输出行和“2:”以表示标准错误行?我认为最合理的方法是创建一个类似于“nohup”的进程,该进程接受命令和参数,并在监督下运行它:“magic_trick -- cmd -o whatever”。如果“magic_trick”程序的输出将要发送到tty,则该程序将使用pty(伪终端)执行一些小魔术,因为“stdout”和“stderr”的行为取决于输出是发送到终端、管道还是文件。许多细节待定。 - Jonathan Leffler
@JosephGarvin 有什么进展吗?我也对此很感兴趣。我想要三个文件:stdout_and_stderr.log,stdout.log和stderr.log。 - Xu Wang
5个回答

2
根据我所理解的,这就是你所需要的。首先,我编写了一个小脚本来写入标准输出和标准错误输出。如下所示:
$ cat foo.sh 
#!/bin/bash

echo foo 1>&2
echo bar

然后我像这样运行它:

$ ./foo.sh 2> >(tee stderr | tee -a combined) 1> >(tee stdout | tee -a combined)
foo
bar

我在bash中的结果如下:

$ cat stderr
foo
$ cat stdout 
bar
$ cat combined 
foo
bar

请注意,需要使用-a标志,以便tee不会覆盖其他tee的内容。

2
这不会保持顺序;如果你将foo.sh中的echo语句包装在循环中,你会发现combined并不会严格交替地以foobar结尾。 - ruakh
你确定吗?听起来像是一个时间问题。先执行 tee -a combined 然后再执行 tee std(out|err) 或许就足够了。我今天稍后会试一下。否则,我看不到绕过命名管道的任何方法。 - keks
1
我找到了最易读的答案!我希望在终端上将stdout和stderr分开(我的终端以不同的颜色显示它们),所以我执行了./foo.sh 2> >(tee -a combined.log >&2) 1> >(tee -a combined.log) - 注意stderr的tee中的>&2 - Victor Sergienko
1
@keks,我完全确定ruakh所描述的问题是合法的。没有保证对任一描述符的写入在父进程写入另一个描述符之前完全刷新管道。 - Charles Duffy

1
这是我如何做到的:

exec 3>log ; example_command 2>&1 1>&3 | tee -a log ; exec 3>&-

工作示例

bash$ exec 3>log ; { echo stdout ; echo stderr >&2 ; } 2>&1 1>&3 | \
      tee -a log ; exec 3>&-
stderr
bash$ cat log
stdout
stderr

这是它的工作原理:

exec 3>log 设置文件描述符3,将其重定向到名为log的文件中,直到另行通知。

example_command 为了使其成为一个可工作的示例,我使用了{ echo stdout; echo stderr>&2; }。或者你可以使用ls /tmp doesnotexist来提供输出。

现在需要跳到管道符|,因为bash首先执行此操作。管道符设置了管道,并将文件描述符1重定向到该管道中。因此,STDOUT现在进入管道。

现在我们可以回到从左到右的解释中的下一步:2>&1表示程序错误将转到STDOUT当前指向的位置,即我们刚刚设置的管道中。

1>&3 表示STDOUT被重定向到文件描述符3,我们之前设置它将输出到log文件。因此,命令的STDOUT只进入日志文件,而不是终端的STDOUT。

tee -a log 从管道中获取输入(你会记得现在管道的输入是命令的错误输出),并将其输出到 STDOUT,同时将其追加到 log 文件。

exec 3>&- 关闭文件描述符 3。


尝试用一个交替的for循环来替换你的一对echo。使用10,000次迭代运行此代码,结果与人们所期望的均匀的out/err/out/err结果根本不接近。 - Charles Duffy

1
可以确保顺序。以下是一个示例,它以生成的顺序捕获标准输出和错误,将其记录到日志文件中,并在任何您喜欢的终端屏幕上仅显示标准错误。根据需要进行调整。

1.打开两个窗口(shell)

2.创建一些测试文件

touch /tmp/foo /tmp/foo1 /tmp/foo2

3.在 window1 中:

mkfifo /tmp/fifo
</tmp/fifo cat - >/tmp/logfile

4.然后,在window2中:
(ls -l /tmp/foo /tmp/nofile /tmp/foo1 /tmp/nofile /tmp/nofile; echo successful test; ls /tmp/nofile1111) 2>&1 1>/tmp/fifo | tee /tmp/fifo 1>/dev/pts/1

/dev/pts/1可以是任何你想要的终端显示。子shell按顺序运行一些“ls”和“echo”命令,有些成功(提供stdout),有些失败(提供stderr),以生成混合的输出和错误消息流,以便您可以在日志文件中验证正确的顺序。


“ls”启动所需的时间比在低负载条件下刷新管道所需的时间长得多。这个测试完全无法证明在紧密竞争中顺序被保留。 - Charles Duffy
此外,OP希望标准输出也能在终端上显示。 - Charles Duffy

1
维克托·谢尔盖恩科(Victor Sergienko)的评论对我有用,将exec放在命令前面使整个脚本都能运行(而不是在单独的命令后面添加)。 exec 2> >(tee -a output_file >&2) 1> >(tee -a output_file)

1
这并不确保顺序被保留。如果您的stderr和stdout写入非常接近,它们可能会以相反的顺序显示在output_file(或TTY)中! - Charles Duffy

1
{ { cmd | tee out >&3; } 2>&1 | tee err >&2; } 3>&1

或者,严谨一点说:

{ { cmd 3>&- | tee out >&3 2> /dev/null; } 2>&1 | tee err >&2 3>&- 2> /dev/null; } 3>&1

请注意,试图保持顺序是徒劳无功的。这基本上是不可能的。唯一的解决方案是修改 "cmd" 或使用一些 LD_PRELOADgdb hack。

那应该可以(我没有尝试过),但是它会写入两个文件(outerr),但问题是是否可能将它们合并到一个文件中。 - Marian

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