Python:subprocess.call,stdout to file,stderr to file,实时在屏幕上显示stderr

Ben*_* S. 27 python subprocess stderr

我有一个命令行工具(实际上是几个),我正在用Python编写包装器.

该工具通常使用如下:

 $ path_to_tool -option1 -option2 > file_out
Run Code Online (Sandbox Code Playgroud)

用户将输出写入file_out,并且还能够在工具运行时查看该工具的各种状态消息.

我想复制此行为,同时还将stderr(状态消息)记录到文件中.

我有的是这个:

from subprocess import call
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file)
Run Code Online (Sandbox Code Playgroud)

除了stderr没有写入屏幕之外,这工作正常.我当然可以添加代码来将log_file的内容打印到屏幕上,但是用户将在完成所有操作后看到它,而不是在它发生时.

总结一下,期望的行为是:

  1. 使用call()或subprocess()
  2. 将stdout直接发送到文件
  3. 将stderr直接写入文件,同时还将stderr实时写入屏幕,就像直接从命令行调用该工具一样.

我有一种感觉,我要么错过了一些非常简单的东西,要么这比我想象的要复杂得多......感谢您的帮助!

编辑:这只需要在Linux上工作.

aba*_*ert 59

可以这样做subprocess,但这不是微不足道的.如果查看文档中的常用参数,您将看到可以PIPE作为stderr参数传递,它创建一个新管道,将管道的一侧传递给子进程,并使另一侧可用作该stderr属性*.

因此,您需要为该管道提供服务,写入屏幕和文件.一般来说,获取正确的细节非常棘手.**在您的情况下,只有一个管道,并且您计划同步维护它,所以它并没有那么糟糕.

import subprocess
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
for line in proc.stderr:
    sys.stdout.write(line)
    log_file.write(line)
proc.wait()
Run Code Online (Sandbox Code Playgroud)

(注意,使用for line in proc.stderr:-basically时存在一些问题,如果您正在阅读的内容因任何原因而不是行缓冲,您可以坐在那里等待换行,即使实际上有半行数据需要处理.你可以一次读取块,比如说,read(128)甚至read(1)可以在必要的时候更顺畅地获取数据.如果你需要在收到每个字节后立即得到它,并且付不起费用read(1),你就会需要将管道置于非阻塞模式并异步读取.)


但是如果你在Unix上,使用tee命令为你做这件事可能会更简单.

对于快速而肮脏的解决方案,您可以使用shell来管理它.像这样的东西:

subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True,
                stdout=file_out)
Run Code Online (Sandbox Code Playgroud)

但我不想调试外壳管道; 让我们在Python中做,如文档中所示:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=file_out, stderr=subprocess.PIPE)
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr)
tool.stderr.close()
tee.communicate()
Run Code Online (Sandbox Code Playgroud)

最后,有一打或周围的子过程和/或在PyPI-壳更多更高级别的包装sh,shell,shell_command,shellout,iterpipes,sarge,cmd_utils,commandwrapper,等等搜索"壳","子","过程","命令行"等等,找到你喜欢的那个让问题变得微不足道.


如果你需要收集stderr和stdout怎么办?

正如Sven Marnach在评论中所建议的那样,简单的方法就是将其中一个重定向到另一个.只需更改这样的Popen参数:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
Run Code Online (Sandbox Code Playgroud)

然后在你使用的任何地方tool.stderr,使用tool.stdout替代 - 例如,为最后一个例子:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout)
tool.stdout.close()
tee.communicate()
Run Code Online (Sandbox Code Playgroud)

但这有一些权衡.最明显的是,将两个流混合在一起意味着您无法将stdout记录到file_out和stderr到log_file,或者将stdout复制到stdout和stderr到stderr.但它也意味着排序可能是非确定性的 - 如果子进程总是在向stdout写入任何内容之前将两行写入stderr,那么一旦混合流,最终可能会在这两行之间得到一堆stdout.这意味着他们必须共享stdout的缓冲模式,所以如果你依赖于linux/glibc保证stderr是行缓冲的事实(除非子进程明确地改变它),那可能不再是真的.


如果您需要单独处理这两个进程,则会变得更加困难.早些时候,我说只要你只有一个管道并且可以同步维修,就可以轻松维修管道.如果你有两个管道,那显然不再是真的.想象一下,你正在等待tool.stdout.read(),新的数据来自tool.stderr.如果数据太多,则可能导致管道溢出并阻止子进程.但即使没有发生这种情况,在stdout出现问题之前,你显然无法读取和记录stderr数据.

如果您使用直通tee解决方案,这可以避免最初的问题......但只能创建一个同样糟糕的新项目.你有两个tee实例,当你打电话communicate给一个时,另一个坐在那里等待永远.

因此,无论哪种方式,您都需要某种异步机制.你可以做到这一点是线程,select反应堆,类似的东西等gevent.

这是一个快速而肮脏的例子:

proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
                        stdout=subprocess.PIPE, stderr=subprocess.PIPE)
def tee_pipe(pipe, f1, f2):
    for line in pipe:
        f1.write(line)
        f2.write(line)
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout))
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr))
t3 = threading.Thread(proc.wait)
t1.start(); t2.start(); t3.start()
t1.join(); t2.join(); t3.join()
Run Code Online (Sandbox Code Playgroud)

但是,有些边缘情况不起作用.(问题是SIGCHLD和SIGPIPE/EPIPE/EOF到达的顺序.我不认为这会影响我们,因为我们没有发送任何输入......但是如果不考虑它就不要相信我通过和/或测试.)subprocess.communicate3.3+ 的功能可以正确地获取所有细节.但是你可能会发现使用PyPI和ActiveState上的async-subprocess包装器实现之一,甚至是像Twisted这样的完整异步框架的子进程内容更简单.


*文档并没有真正解释管道是什么,几乎就像他们希望你是一个旧的Unix C手...但是一些例子,特别是在subprocess模块部分替换旧函数时,展示了它们是如何被使用的,这很简单.

**困难的部分是正确排序两个或多个管道.如果你在一个管道上等待,另一个可能会溢出并阻塞,阻止你在另一个管道上等待.解决这个问题的唯一简单方法是创建一个服务每个管道的线程.(在大多数*nix平台上,您可以使用selectpoll反应堆,但是制作跨平台非常困难.)模块的来源,特别是communicate它的助手,显示了如何做到这一点.(我链接到3.3,因为在早期版本中,communicate本身会出现一些重要的错误...)这就是为什么,只要有可能,你想要使用,communicate如果你需要多个管道.在你的情况下,你不能使用communicate,但幸运的是你不需要多个管道.

  • 第一个代码示例中的`data = proc.stderr.read()`阻塞,直到读取*all*数据. (2认同)