2013-08-20 25 views
23

我有一个命令行工具(实际上是几个),我正在为Python编写一个包装器。Python:subprocess.call,标准输出到文件,stderr到文件,实时在屏幕上显示stderr

的工具一般是用这样的:

$ path_to_tool -option1 -option2 > file_out 

用户得到写入file_out输出,并且还能够看到工具的各种状态信息,因为它正在运行。

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

我有这样的:

from subprocess import call 
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file) 

这工作得很好除非该标准错误不被写入到屏幕上。 我可以添加代码来将log_file的内容打印到屏幕当然,但是用户在完成所有事情之后会看到它,而不是在它发生的时候。

总括来说,所需的行为是:

  1. 使用()调用,或子()
  2. 直接标准输出到文件
  3. 直接错误输出到一个文件,同时还写错误输出到屏幕实时仿佛 工具直接从命令行调用。

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

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

+0

请问你的代码需要在Windows(或其他非POSIXy平台)工作的?如果不是,则有一个更简单的答案。 – abarnert

+0

它不需要! –

+0

相关:[Python子进程获取儿童输出到文件和终端?](http://stackoverflow.com/q/4984428/4279) – jfs

回答

52

可以做到这一点与subprocess,但它不是微不足道的。如果您查看文档中的Frequently Used Arguments,您会发现可以通过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() 

(注意有使用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) 

但我不想调试壳管道;让我们做它在Python,如图in the docs

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() 

最后,有十几个或更多的围绕子流程和/或PyPI- shshellshell_commandshellout外壳更高层次的包装,搜索“shell”,“subprocess”,“process”,“command line”等,找到一个你喜欢的,这使得问题变得微不足道。


如果您需要收集stderr和stdout,该怎么办?

最简单的方法就是将其中一个重定向到另一个,就像Sven Marnach在评论中提出的那样。只要改变Popen参数是这样的:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'], 
         stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 

然后到处使用tool.stderr,使用tool.stdout代替-e.g,对于最后一个例子:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout) 
tool.stdout.close() 
tee.communicate() 

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


如果您需要分开处理这两个过程,则会变得更加困难。早些时候,我说过,只要你只有一根管道并且可以同步维修,就可以轻松地维修管道。如果你有两个管道,那显然不再是真的。想象一下,你正在等待tool.stdout.read(),新数据来自tool.stderr。如果数据太多,可能会导致管道溢出并阻塞子进程。但即使这种情况没有发生,您显然将无法读取并记录stderr数据,直到从stdout中输入内容为止。

如果您使用pipe-through-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() 

然而,也有一些边缘情况下,这是行不通的。 (问题在于SIGCHLD和SIGPIPE/EPIPE/EOF到达的顺序,我不认为这会影响到我们,因为我们没有发送任何输入信息......但是不要相信我通过和/或测试)。从3.3+的subprocess.communicate函数获得所有的细节。但是您可能会发现使用PyPI和ActiveState上可以找到的一个异步子进程包装器实现,或者甚至是像Twisted这样的完整异步框架中的子进程内容,都会更加简单。


*的文档并不真正说明什么管道是,仿佛他们希望你是一个老Unix下C手......但一些例子,特别是在Replacing Older Functions with the subprocess Module部分,展示他们如何是使用,而且非常简单。

**困难的部分是正确排序两个或多个管道。如果你等待一个管道,另一个可能会溢出并阻塞,从而阻止你等待另一个管道完成。解决这个问题的唯一简单方法是创建一个线程来服务每个管道。 (在大多数* nix平台上,您可以使用selectpoll电抗器,但是使该跨平台非常困难。)The source模块,特别是communicate及其帮助程序显示如何执行此操作。 (我链接到3.3,因为在早期版本中,communicate本身有一些重要的错误...)这就是为什么,只要有可能,如果您需要多个管道,则要使用communicate。在你的情况下,你不能使用communicate,但幸运的是你不需要多个管道。

+1

非常有帮助,谢谢。你的意思是写p1和p2吗? –

+0

@ user2063292:对不起,这是'tool'和'tee'。遵循示例代码太紧密。 :)谢谢你抓到它。 – abarnert

+0

'2 |'是否应该标记为stderr?它不在POSIX shell中。 –

0

我想你正在寻找的是这样的:

import sys, subprocess 
p = subprocess.Popen(cmdline, 
        stdout=sys.stdout, 
        stderr=sys.stderr) 

要使输出/日志写入一个文件我想修改我的cmdline包括通常的重定向,因为它会在一个普通的做linux bash/shell。例如,我会在命令行中追加teecmdline += ' | tee -a logfile.txt'

希望有所帮助。

0

我不得不做出的Python 3. @ abarnert的回答一些变化这似乎是工作:

def tee_pipe(pipe, f1, f2): 
    for line in pipe: 
     f1.write(line) 
     f2.write(line) 

proc = subprocess.Popen(["/bin/echo", "hello"], 
         stdout=subprocess.PIPE, 
         stderr=subprocess.PIPE) 

# Open the output files for stdout/err in unbuffered mode. 
out_file = open("stderr.log", "wb", 0) 
err_file = open("stdout.log", "wb", 0) 

stdout = sys.stdout 
stderr = sys.stderr 

# On Python3 these are wrapped with BufferedTextIO objects that we don't 
# want. 
if sys.version_info[0] >= 3: 
    stdout = stdout.buffer 
    stderr = stderr.buffer 

# Start threads to duplicate the pipes. 
out_thread = threading.Thread(target=tee_pipe, 
           args=(proc.stdout, out_file, stdout)) 
err_thread = threading.Thread(target=tee_pipe, 
           args=(proc.stderr, err_file, stderr)) 

out_thread.start() 
err_thread.start() 

# Wait for the command to finish. 
proc.wait() 

# Join the pipe threads. 
out_thread.join() 
err_thread.join() 
相关问题