Python:subprocess.call,stdout到文件,stderr到文件,在屏幕上实时显示stderr

Python: subprocess.call, stdout to file, stderr to file, display stderr on screen in real time

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

该工具通常是这样使用的:

1
 $ path_to_tool -option1 -option2 > file_out

用户将输出写入file_out,并且还可以在工具运行时查看其各种状态消息。

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

我所拥有的是:

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

除未将stderr写入屏幕外,此方法都可以正常工作。
我当然可以添加代码以将log_file的内容打印到屏幕上,但是随后用户将在完成所有操作后而不是在执行操作时看到它。

概括地说,所需的行为是:

  • 使用call()或subprocess()
  • 直接将标准输出到文件
  • 将stderr定向到文件,同时还将stderr实时写入屏幕,就像
    该工具已直接从命令行调用。
  • 我觉得我要么错过了一些非常简单的事情,要么比我想的要复杂得多……感谢您的帮助!

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


    您可以使用subprocess来做到这一点,但这并不简单。如果您查看文档中的"常用参数",您会看到可以将PIPE作为stderr参数传递,这将创建一个新管道,将该管道的一侧传递给子进程,并使另一面可用作stderr属性。*

    好。

    因此,您将需要维修该管道,并写入屏幕和文件。通常,正确地获取详细信息非常棘手。**在您的情况下,只有一个管道,并且您打算同步维护它,所以还不错。

    好。

    1
    2
    3
    4
    5
    6
    7
    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命令为您完成操作可能会更简单。

    好。

    对于快速,肮脏的解决方案,您可以使用外壳通过管道进行管理。像这样:

    好。

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

    但是我不想调试外壳管道。让我们在python中进行操作,如文档所示:

    好。

    1
    2
    3
    4
    5
    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周围有十几个或更多个更高级别的包装器-shshellshell_commandshelloutiterpipessargecmd_utilscommandwrapper等。搜索" shell","子进程","进程","命令行"等,然后找到您喜欢的那个,使问题变得微不足道。

    好。

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

    好。

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

    好。

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

    然后在所有使用tool.stderr的地方,都改为使用tool.stdout-例如,对于最后一个示例:

    好。

    1
    2
    3
    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之类的东西来执行此操作。

    好。

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

    好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的老手一样……但是,其中的一些示例,特别是在用subprocess Module替换旧函数中,展示了它们是如何工作的。重新使用,这很简单。

    好。

    **困难的部分是正确地对两个或多个管道进行排序。如果您在一根管道上等待,另一根管道可能会溢出并阻塞,从而导致您无法继续等待另一根管道。解决此问题的唯一简单方法是创建一个线程来服务每个管道。 (在大多数* nix平台上,可以改为使用selectpoll反应堆,但要使该跨平台变得异常困难。)该模块的源代码,尤其是communicate及其助手,显示了如何做它。 (我链接到3.3,因为在早期版本中,communicate本身会犯一些重要的错误……)因此,如果需要多个管道,则尽可能使用communicate。就您而言,您不能使用communicate,但是幸运的是,您不需要多个管道。

    好。

    好。


    我必须对@abarnert对于Python 3的答案进行一些更改。这似乎可行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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()


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

    1
    2
    3
    4
    import sys, subprocess
    p = subprocess.Popen(cmdline,
                         stdout=sys.stdout,
                         stderr=sys.stderr)

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

    希望能有所帮助。