按时间顺序捕获 STDOUT 和 STDERR

Chronologically capturing STDOUT and STDERR

这很可能属于 KISS(保持简单)原则,但我仍然很好奇并希望了解为什么我没有收到预期结果。那么,我们开始吧...

我有一个 shell 脚本来捕获 STDOUT 和 STDERR 而不会干扰原始文件描述符。这是为了保留用户在终端上看到的原始输出顺序(请参阅下面的 test.pl)。

不幸的是,我仅限于使用 sh,而不是 bash(但我欢迎示例),因为我是从另一个套件调用它的,我可能希望将来在 cron 中使用它(我知道 cron 有 SHELL 环境变量)。

wrapper.sh 包含:

#!/bin/sh
stdout_and_stderr=
shift
command=$@

out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
mkfifo ${out} ${err}
trap 'rm ${out} ${err}' EXIT
> ${stdout_and_stderr}
tee -a ${stdout_and_stderr} < ${out} &
tee -a ${stdout_and_stderr} < ${err} >&2 &
${command} >${out} 2>${err}

test.pl 包含:

#!/usr/bin/perl

print "1: stdout1\n";
print STDERR "2: stderr1\n";
print "3: stdout2\n";

场景中:

sh wrapper.sh /tmp/xxx perl test.pl

STDOUT 包含:

1: stdout1
3: stdout2

STDERR 包含:

2: stderr1

到目前为止一切都很好...

/tmp/xxx 包含:

2: stderr1
1: stdout1
3: stdout2

但是,我希望 /tmp/xxx 包含:

1: stdout1
2: stderr1
3: stdout2

任何人都可以向我解释为什么 STDOUT 和 STDERR 没有按照我预期的顺序附加 /tmp/xxx 吗?我的猜测是后台 tee 进程正在相互阻塞 /tmp/xxx 资源,因为它们具有相同的 "destination"。你会如何解决这个问题?

相关:How do I write stderr to a file while using "tee" with a pipe?

stderr 没有缓冲是 C 运行时库的一个特性(并且可能被其他运行时库模仿)。一旦写入,stderr 就会将其所有字符推送到目标设备。

默认情况下 stdout 有一个 512 字节的缓冲区。

stderrstdout 的缓冲可以通过 setbuf or setvbuf 调用更改。

来自标准输出的 Linux 手册页:

NOTES: The stream stderr is unbuffered. The stream stdout is line-buffered when it points to a terminal. Partial lines will not appear until fflush(3) or exit(3) is called, or a newline is printed. This can produce unexpected results, especially with debugging output. The buffering mode of the standard streams (or any other stream) can be changed using the setbuf(3) or setvbuf(3) call. Note that in case stdin is associated with a terminal, there may also be input buffering in the terminal driver, entirely unrelated to stdio buffering. (Indeed, normally terminal input is line buffered in the kernel.) This kernel input handling can be modified using calls like tcsetattr(3); see also stty(1), and termios(3).

在@wallyk 的启发下进行了更多搜索后,我对 wrapper.sh 进行了以下修改:

#!/bin/sh
stdout_and_stderr=
shift
command=$@

out="${TMPDIR:-/tmp}/out.$$"
err="${TMPDIR:-/tmp}/err.$$"
mkfifo ${out} ${err}
trap 'rm ${out} ${err}' EXIT
> ${stdout_and_stderr}
tee -a ${stdout_and_stderr} < ${out} &
tee -a ${stdout_and_stderr} < ${err} >&2 &
script -q -F 2 ${command} >${out} 2>${err}

现在产生预期的:

1: stdout1
2: stderr1
3: stdout2

解决方案是在 $command 前加上 script -q -F 2 前缀,这使得 script 相当 (-q),然后强制文件描述符 2 (STDOUT) 立即刷新 ( -F 2).

我现在正在研究以确定它的便携性。我认为 -F pipe 可能是 Mac 和 FreeBSD,而 -f--flush 可能是其他发行版...

相关:How to make output of any shell command unbuffered?