在管道中强制对stdout进行行缓冲处理

170
通常情况下,stdout 是行缓冲的。换句话说,只要您的 printf 参数以换行符结尾,就可以期望该行立即打印出来。然而,当使用管道重定向到 tee 时,这种情况似乎不成立。
我有一个 C++ 程序,名为 a,总是将字符串输出到 stdout 中(每个字符串都以 \n 结尾)。
当独立运行程序 (./a) 时,所有内容都会按时正确打印,但是如果我将它传递给 tee./a | tee output.txt),它直到退出之前都不会打印任何内容,这违反了使用 tee 的目的。
我知道我可以通过在 C++ 程序中的每次打印操作后添加 fflush(stdout) 来解决问题。但是是否有更简洁、更容易的方法?例如,是否有命令可以运行,强制 stdout 在使用管道时也是行缓冲的?
7个回答

170
您可以尝试使用stdbuf
$ stdbuf --output=L ./a | tee output.txt

(大部分) man 页面:

  -i, --input=MODE   adjust standard input stream buffering
  -o, --output=MODE  adjust standard output stream buffering
  -e, --error=MODE   adjust standard error stream buffering

If MODE is 'L' the corresponding stream will be line buffered.
This option is invalid with standard input.

If MODE is '0' the corresponding stream will be unbuffered.

Otherwise MODE is a number which may be followed by one of the following:
KB 1000, K 1024, MB 1000*1000, M 1024*1024, and so on for G, T, P, E, Z, Y.
In this case the corresponding stream will be fully buffered with the buffer
size set to MODE bytes.

记住,不过要牢记以下内容:
NOTE: If COMMAND adjusts the buffering of its standard streams ('tee' does
for e.g.) then that will override corresponding settings changed by 'stdbuf'.
Also some filters (like 'dd' and 'cat' etc.) dont use streams for I/O,
and are thus unaffected by 'stdbuf' settings.

你没有在tee上运行stdbuf,而是在a上运行它,所以这不会影响你,除非你在a的源中设置了流的缓冲。

此外,stdbuf不是POSIX标准的一部分,而是GNU-coreutils的一部分。


3
谢谢,但似乎这个在OS X上不可用(该问题被标记为osx-lion)。 - houbysoft
3
我很确定GNU工具可以在OS X上安装。 - jordanm
2
@jordanm:也许是这样,但安装整个GNU工具集似乎有点过头了... - houbysoft
3
因为我们使用的Centos Linux发行版已经有了stdbuf,而没有unbuffer,所以我点赞了这个答案。谢谢! - Huw Walters
31
对于 Python 脚本,stdbuf 将无法工作,但是您可以使用“-u”选项来禁用 Python 一侧的缓冲:python3 -u a.py | tee output.txt - Honza
显示剩余3条评论

87

尝试使用unbufferman页面),它是expect软件包的一部分。你的系统上可能已经有了它。

在你的情况下,你可以这样使用:

unbuffer ./a | tee output.txt

-p选项是用于管道模式,其中unbuffer从stdin读取并将其传递给剩余参数中的命令。


3
我已经通过brew在我的Mac(10.8.5)上安装了它:brew install expect --with-brewed-tk - Nils
2
就这个问题而言,因为unbuffer有点不直观,相关结构是 unbuffer {带管道/tee的命令} - Fake Name
我必须这样安装它:brew install homebrew/dupes/expect - nhed
1
我认为应该使用"unbuffer ./a | tee output.txt"而不是"tee"需要取消缓冲。至少对于类似的问题,这对我有效。 - Hugo van der Sanden
1
我认为正确的命令,如果你读了 man 手册,应该是 unbuffer ./a | tee output.txt。这是在 RPi 上使用 bash 和 tmux 时对我有效的方法。 - berezovskyi
显示剩余6条评论

38
您可以使用stdio.h中的setlinebuf。
setlinebuf(stdout);

这应该将缓冲更改为“行缓冲”。

如果需要更多的灵活性,可以使用setvbuf。


10
我在想为什么这个解决方案的赞数这么少。这是唯一一个不给调用者带来负担的解决方案。 - oxygene
5
请注意,这不是标准的C语言(甚至不是POSIX标准)。最好使用 setvbuf(stdout, NULL, _IOLBF, 0),它完全等效。 - rvighne
这个解决了我在使用C++程序printf()并将其管道传输到tee时,在OS X Catalina上遇到的问题,但只有在程序完成后才能看到输出。 - jbaxter

34

你也可以尝试在伪终端中使用script命令来执行您的命令(该命令应该强制将输出行缓冲到管道中)!

script -q /dev/null ./a | tee output.txt     # Mac OS X, FreeBSD
script -c "./a" /dev/null | tee output.txt   # Linux

请注意,script 命令不会传递包装命令的退出状态。


5
“script -t 1 /path/to/outputfile.txt ./a” 对于我的使用情况非常好用。它将所有输出实时流式传输到“outputfile.txt”,同时也打印到您的shell的stdout中。我不需要使用tee - Peter Berg
util-linux和BSD中的script都提供了-e选项,以返回执行命令的退出状态。 - Socowi

9

来自@Paused until further notice答案的expect包中的unbuffer命令对我没有按照它所呈现的方式起作用。

我不得不使用以下命令:

unbuffer -p ./a | tee output.txt

而不是使用:

./a | unbuffer -p tee output.txt

(-p是管道模式,其中unbuffer从stdin读取并将其传递给其余参数中的命令)

expect包可以安装在以下操作系统上:

  1. 使用pacman -S expect在MSYS2上
  2. 使用brew install expect在Mac OS上

更新

最近我在一个shell脚本中使用python时遇到了缓冲问题(尝试将时间戳附加到其输出中)。解决方法是向python传递-u标志,如下:

  1. run.sh 带有 python -u script.py
  2. unbuffer -p /bin/bash run.sh 2>&1 | tee /dev/tty | ts '[%Y-%m-%d %H:%M:%S]' >> somefile.txt
  3. 此命令将在输出上放置时间戳,并将其同时发送到文件和标准输出。
  4. ts程序(时间戳)可通过moreutils软件包安装。

更新2

最近,当我在grep上使用参数grep --line-buffered以停止缓冲输出时,也遇到了grep缓冲输出的问题。


我也遇到了同样的问题。这种方法可以解决它。 - Carlos Pinzón
你用 Python 的东西让我开心了一整天!顺便说一句,你也可以使用环境变量来实现同样的效果: export PYTHONUNBUFFERED=1 - Airstriker
如果您需要在bash脚本中记录日志,这是一个非常好的设置:#将所有输出发送到一个文件和屏幕 LOGFILE=some_file.log set -x #在每行前加上时间戳并打印出来(无缓冲) exec > >(ts“[%F %H:%M:%S]”| unbuffer -p tee ${LOGFILE}) 2>&1 #强制python的stdout和stderr流不带缓冲; export PYTHONUNBUFFERED=1 - Airstriker

3
如果您使用C++流类,那么每个std::endl都是隐式刷新。如果使用C风格的打印,我认为您提出的方法(fflush())是唯一的方法。

4
很遗憾,这不是真的。即使在使用 std::endl 或 std::flush 时,你也可以观察到与 c++ std::cout 相同的行为。缓冲发生在上层,而在 Linux 中最简单的解决方法似乎是在 main() 函数中作为第一行使用 setlinebuf(stdout);,如果您是程序的作者,并且当无法更改源代码时,使用上述其他解决方案。 - oxygene
2
@oxygene 这不是真的。我已经试过了,当使用管道到tee时endl确实会刷新缓冲区(与printf不同)。 代码:#include <iostream> #include <unistd.h> int main(void) { std::cout << "1" << std::endl; sleep(1); std::cout << "2" << std::endl; }。根据这里的定义,endl总是会刷新缓冲区:http://en.cppreference.com/w/cpp/io/manip/endl - Curtis Yallop

1

我来这里是为了添加 grep --line-buffered 作为一个简单的替代方案... 然而,我注意到在多个并行进程中,有时仍然会看到被合并的行(但至少一次性合并完整的行,而不是随机列交错的行...) - Ajax

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