30

我有一个命令行工具(实际上是几个),我正在用 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 的内容打印到屏幕上,但是用户将在一切完成后看到它,而不是在它发生时看到它。

回顾一下,期望的行为是:

  1. 使用 call() 或 subprocess()
  2. 将标准输出定向到文件
  3. 将 stderr 直接写入文件,同时还将 stderr 实时写入屏幕,就像直接从命令行调用该工具一样。

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

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

4

3 回答 3

66

可以用 来做到这一点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:,如果您正在阅读的内容因任何原因被证明不是行缓冲的,即使实际上有半行数据需要处理,您也可以坐等换行。您可以一次读取块,例如,read(128)甚至read(1),以便在必要时更顺利地获取数据。如果您需要在每个字节到达后立即获取它,并且负担不起 的成本read(1),您将需要将管道置于非阻塞模式并异步读取。)


但是,如果您使用的是 Unix,则使用该tee命令为您执行此操作可能会更简单。

要获得快速而肮脏的解决方案,您可以使用外壳通过它进行管道传输。像这样的东西:

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 上围绕子进程和/或 shell 有十几个或更多更高级别的包装器——<code>sh, shell, shell_command, shellout, iterpipes, sarge, cmd_utils,commandwrapper等。搜索“shell”、“subprocess”、“process”, “命令行”等,并找到一个你喜欢的,使问题变得微不足道。


如果您需要同时收集 stderr 和 stdout 怎么办?

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

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

然后在你使用的任何地方tool.stderrtool.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 中输入某些内容。

如果您使用管道直通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.communicate3.3+ 中的功能正确地获取了所有繁琐的细节。但是你可能会发现使用你可以在 PyPI 和 ActiveState 上找到的异步子进程包装器实现之一,甚至是来自像 Twisted 这样成熟的异步框架的子进程的东西要简单得多。


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

** 困难的部分是正确排序两个或多个管道。如果您在一个管道上等待,另一个可能会溢出并阻塞,从而阻止您对另一个管道的等待完成。解决这个问题的唯一简单方法是创建一个线程来服务每个管道。(在大多数 *nix 平台上,您可以使用 a selector pollreactor 代替,但要实现跨平台非常困难。)模块的源代码,尤其是communicate它的助手,展示了如何做到这一点。(我链接到 3.3,因为在早期版本中,它本身会弄错一些重要的事情……)这就是为什么在需要多个管道communicate时尽可能使用的原因。communicate在您的情况下,您不能使用communicate,但幸运的是您不需要多个管道。

于 2013-08-20T21:16:22.573 回答
1

我认为你正在寻找的是这样的:

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

要将输出/日志写入文件,我会修改我cmdline的以包含通常的重定向,就像在普通的 linux bash/shell 上完成的那样。例如,我将追加tee到命令行:cmdline += ' | tee -a logfile.txt'

希望有帮助。

于 2016-09-11T12:20:34.940 回答
1

我不得不对@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()
于 2017-11-16T12:13:38.930 回答