
[英]Python subprocess.call() add prefix to each stdout and stderr line
[英]Python: subprocess.call, stdout to file, stderr to file, display stderr on screen in real time
我有一个命令行工具(实际上是几个),我正在用Python编写包装器。
该工具通常使用如下:
$ 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)
除了stderr没有写入屏幕之外,这工作正常。 我当然可以添加代码来将log_file的内容打印到屏幕上,但是用户将在完成所有操作后看到它,而不是在它发生时。
总结一下,期望的行为是:
我有一种感觉,我要么错过了一些非常简单的东西,要么这比我想象的要复杂得多......感谢您的帮助!
编辑:这只需要在Linux上工作。
您可以使用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()
(请注意, for line in proc.stderr:
使用for line in proc.stderr:
存在一些问题for line in proc.stderr:
,如果您正在阅读的内容因任何原因而不是行缓冲,您可以坐在那里等待换行,即使实际有一半要处理的数据行。你可以一次读取块,比如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中做,如文档中所示:
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- sh
, shell
, shell_command
, shellout
, iterpipes
, sarge
, cmd_utils
, commandwrapper
等的子进程和/或shell周围有十几个或更多高级包装器。搜索“shell”,“ commandwrapper
”, “进程”,“命令行”等,并找到一个你喜欢的问题,使问题变得微不足道。
如果你需要收集stderr和stdout怎么办?
正如Sven Marnach在评论中所建议的那样,简单的方法就是将其中一个重定向到另一个。 只需更改Popen
参数,如下所示:
tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
然后在你使用tool.stderr
任何地方,使用tool.stdout
代替 - 例如,对于最后一个例子:
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
。 如果数据太多,则可能导致管道溢出并阻止子进程。 但即使没有发生这种情况,在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()
但是,有些边缘情况不起作用。 (问题是SIGCHLD和SIGPIPE / EPIPE / EOF到达的顺序。我不认为这会影响我们,因为我们没有发送任何输入......但是如果不考虑它就不要相信我通过和/或测试)的subprocess.communicate
从3.3+功能得到所有的繁琐细节的权利。 但是你可能会发现使用PyPI和ActiveState上的async-subprocess包装器实现之一,甚至是像Twisted这样的完整异步框架的子进程内容更简单。
*文档并没有真正解释什么是管道,几乎就像他们希望你是一个旧的Unix C手......但是一些例子,特别是在用subprocess
模块部分替换旧函数中 ,展示了它们是如何被使用的,这很简单。
**困难的部分是正确排序两个或多个管道。 如果你在一个管道上等待,另一个可能会溢出并阻塞,阻止你在另一个管道上等待。 解决这个问题的唯一简单方法是创建一个服务每个管道的线程。 (在大多数* nix平台上,您可以使用select
或poll
反应器,但是制作该跨平台非常困难。)模块的源代码 ,尤其是communicate
及其帮助程序,显示了如何执行此操作。 (我链接到3.3,因为在早期版本中, communicate
本身会导致一些重要的错误...)这就是为什么,如果需要多个管道,只要有可能,就要使用communicate
。 在您的情况下,您不能使用communicate
,但幸运的是,您不需要多个管道。
我认为你在寻找的是:
import sys, subprocess
p = subprocess.Popen(cmdline,
stdout=sys.stdout,
stderr=sys.stderr)
要将输出/日志写入文件,我将修改我的cmdline
以包含通常的重定向,因为它将在普通的linux bash / shell上完成。 例如,我会将tee
附加到命令行: cmdline += ' | tee -a logfile.txt'
cmdline += ' | tee -a logfile.txt'
希望有所帮助。
我不得不对@ abarnert对Python 3的回答做一些修改。这似乎有效:
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()
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.