我有一个命令行工具(实际上是几个),我正在用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
做到这一点,但这并不简单。如果您查看文档中的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:
会出现一些问题-基本上,如果您正在阅读的内容由于某种原因而没有被行缓冲,即使实际上有一半的数据需要处理,您也可以坐在那里等待换行您可以使用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)
但是我不想调试 shell 管道。让我们在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上的子进程和/或shell周围有十几个或更高级的包装器-
sh
,shell
,shell_command
,shellout
,iterpipes
,sarge
,cmd_utils
,commandwrapper
等。搜索“shell”,“subprocess”, “进程”,“命令行”等,然后找到您喜欢的解决方案,从而使问题变得微不足道。如果您需要同时收集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
。如果数据太多,则可能导致管道溢出和子进程阻塞。但是,即使那没有发生,您也显然无法读取和记录stderr数据,除非有stdout发出一些信息。如果使用pipe-through-
tee
解决方案,则可以避免最初的问题...但是只能通过创建一个同样糟糕的新项目来解决。您有两个tee
实例,而当您在一个实例上调用communicate
时,另一个实例就坐在那里永远等待。因此,无论哪种方式,您都需要某种异步机制。您可以通过线程,
select
react 堆,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平台上,您可以改用
select
或poll
react 器,但是要使该跨平台变得异常困难。)模块的The source,尤其是communicate
及其助手,显示了如何执行此操作。 (我链接到3.3,因为在较早的版本中,communicate
本身会犯一些重要的错误……)因此,如果需要多个管道,则尽可能使用communicate
。就您而言,您不能使用communicate
,但是幸运的是,您不需要多个管道。
关于python - Python:subprocess.call,stdout到文件,stderr到文件,在屏幕上实时显示stderr,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18344932/