【问题标题】:Python's Popen + communicate only returning the first line of stdoutPython Popen + 通信只返回标准输出的第一行
【发布时间】:2017-01-26 15:05:28
【问题描述】:

我正在尝试使用我的命令行 git 客户端和 Python 的 I/O 重定向来自动化许多 git repos 上的一些常见操作。 (是的,这是 hack-ish。我以后可能会回去使用 Python 库来执行此操作,但现在它似乎工作正常 :))

我希望能够捕获调用 git 的输出。隐藏输出看起来会更好,捕获它可以让我记录它以备不时之需。

我的问题是,当我运行“git clone”命令时,我只能得到第一行的输出。奇怪的是,带有“git status”的相同代码似乎工作得很好。

我在 Windows 7 上运行 Python 2.7,并且正在使用 cmd.exe 命令解释器。

到目前为止我的调查:

  1. 当我用“git clone”调用 subprocess.call() 时,它运行良好,我 查看控制台上的输出(确认 git 正在生成 输出,即使我没有捕获它)。这段代码:

    dir = "E:\\Work\\etc\\etc"
    os.chdir(dir)
    git_cmd = "git clone git@192.168.56.101:Mike_VonP/bit142_assign_2.git"
    
    #print "SUBPROCESS.CALL" + "="*20
    #ret = subprocess.call(git_cmd.split(), shell=True) 
    

    将在控制台上产生这个输出:

    SUBPROCESS.CALL====================
    Cloning into 'bit142_assign_2'...
    remote: Counting objects: 9, done.
    remote: Compressing objects: 100% (4/4), done.
    remote: Total 9 (delta 0), reused 0 (delta 0)
    Receiving objects: 100% (9/9), done.
    Checking connectivity... done.
    
  2. 如果我直接用 POpen 做同样的事情,我会看到相同的输出 控制台(也 没有 被捕获)。这段代码:

    # (the dir = , os.chdir, and git_cmd= lines are still executed here)
    print "SUBPROCESS.POPEN" + "="*20
    p=subprocess.Popen(git_cmd.split(), shell=True)
    p.wait()
    

    将产生这个(实际上相同的)输出:

    SUBPROCESS.POPEN====================
    Cloning into 'bit142_assign_2'...
    remote: Counting objects: 9, done.
    remote: Compressing objects: 100% (4/4), done.
    remote: Total 9 (delta 0), reused 0 (delta 0)
    Receiving objects: 100% (9/9), done.
    Checking connectivity... done.
    

    (显然我在运行之间删除了克隆的 repo,否则我会 收到“一切都是最新的”消息)

  3. 如果我使用communicate() 方法,我期望得到一个字符串 包含我在上面看到的所有输出。相反,我只 看线 Cloning into 'bit142_assign_2'....
    这段代码:

    print "SUBPROCESS.POPEN, COMMUNICATE" + "="*20
    p=subprocess.Popen(git_cmd.split(), shell=True,\
                bufsize = 1,\
                stderr=subprocess.PIPE,\
                stdout=subprocess.PIPE)
    tuple = p.communicate()
    p.wait()
    print "StdOut:\n" + tuple[0]
    print "StdErr:\n" + tuple[1]
    

    将产生这个输出:

    SUBPROCESS.POPEN, COMMUNICATE====================
    StdOut:
    
    StdErr:
    Cloning into 'bit142_assign_2'...
    

    一方面,我已经重定向了输出(正如你所看到的那样 它不在输出中)但我也只捕获第一行。

我已经尝试了很多很多东西(调用check_output而不是popen,使用带有subprocess.call的管道,使用带有subprocess.popen的管道,以及可能我忘记的其他东西)但没有任何效果 - 我只捕获第一行输出。

有趣的是,完全相同的代码确实可以与“git status”一起正常工作。一旦 repo 被克隆,调用 git status 会产生三行输出(统称为“一切都是最新的”),第三个示例(POpen+communicate 代码)确实捕获了所有三行输出。

如果有人对我做错了什么有任何想法,或者对我可以尝试的任何事情有任何想法,以便更好地诊断这个问题,我将不胜感激。

【问题讨论】:

  • 关于第三个示例的有趣之处在于,输出行(显然)在 stderr 上,这是我没有预料到的。通信文档(docs.python.org/2.6/library/…)表明元组中的第一项应该是标准输出。被捕获的输出行看起来不像是错误,所以我不确定为什么会在那里报告(我假设 git 正在将它打印到 stderr)

标签: python git popen communicate


【解决方案1】:

这里有两个感兴趣的部分,一个是 Python 特定的,一个是 Git 特定的。

Python

使用subprocess 模块时,您可以选择控制您运行的程序的最多三个I/O 通道:stdin、stdout 和stderr。 subprocess.callsubprocess.check_call 以及 subprocess.Popen 都是如此,但是 callcheck_call 都会立即调用新进程对象的 wait 方法,因此由于各种原因,提供 subprocess.PIPE 是不明智的用于具有这两个操作的标准输出和/或标准错误。1

除此之外,使用subprocess.call 等同于使用subprocess.Popen。事实上,call 的代码是单行的:

def call(*popenargs, **kwargs):
    return Popen(*popenargs, **kwargs).wait()

如果您选择不重定向任何 I/O 通道,读取输入的程序会从 Python 的同一位置获取输入,将输出写入标准输出的程序会将其写入您自己的 Python 代码的同一位置,2 并且将输出写入 stderr 的程序将其写入 Python 会写入的相同位置。

当然,您可以将 stdout 和/或 stderr 重定向到实际文件以及 subprocess.PIPEs。文件和管道不是交互式“终端”或“tty”设备(即,不被视为直接连接到人类)。这将我们引向 Git。

Git

Git 程序通常可以从标准输入读取和/或写入标准输出和/或标准错误。 Git 也可能调用其他程序,这些程序可能会做同样的事情,或者可能会绕过这些标准 I/O 通道。

特别是,git clone 主要写入其标准错误,正如您所观察到的。此外,作为mhawke answered,您必须添加--progress 以使Git 将进度消息写入stderr Git 不与交互式tty 设备对话。

如果 Git 在通过httpsssh 克隆时需要密码或其他身份验证,Git 将运行一个辅助程序来获取此信息。在大多数情况下,这些程序完全绕过 stdin(通过在 POSIX 系统上打开 /dev/tty,或在 Windows 上打开等效项),以便与用户进行交互。在您的自动化环境中,这将如何运作,或者它是否会运作是一个很好的问题(但又超出了此答案的范围)。但这确实让我们回到了 Python,因为 ...

Python

除了subprocess 模块之外,还有一些外部库shpexpect,以及Python 本身内置的一些设施via the pty module,它们可以打开一个伪tty:一个交互式tty 设备,而不是直接连接到人,连接到你的程序。

当使用 ptys 时,您可以让 Git 的行为与它直接与人交谈时的行为相同——事实上,今天“与人交谈”实际上是使用 ptys(或等效项)完成的,因为有程序运行各种窗口系统。此外,要求人类输入密码的程序现在可能3与您自己的 Python 代码交互。这可能是好是坏(甚至两者兼而有之),因此请考虑您是否希望这种情况发生。


1具体来说,communicate 方法的重点是管理最多三个流之间的 I/O 流量,如果它们中的任何一个或全部是 PIPE,则无需子进程楔。想象一下,如果你愿意,一个子进程将 64K 文本打印到 stdout,然后将 64K 文本打印到 stderr,然后再将 64K 文本打印到 stdout,然后从 stdin 读取。如果您尝试以任何特定顺序读取或写入其中任何一个,子进程将“卡住”,等待您清除其他内容,而您将卡住等待子进程完成您选择先完成的任何一个。 communicate 所做的是使用线程或特定于操作系统的非阻塞 I/O 方法来提供子进程输入同时读取其 stdout 和 stderr。

换句话说,它处理多路复用。因此,如果您没有为三个 I/O 通道中的至少 两个 提供subprocess.PIPE,则绕过communicate 方法是安全的。如果您,则不是(除非您实现自己的多路复用)。

这里有一个有点奇怪的边缘情况:如果您为 stderr 输出提供 subprocess.STDOUT,这会告诉 Python 将子进程的两个输出定向到单个通信通道。这仅算作一个管道,因此如果将子进程的 stdout 和 stderr 组合在一起,并且不提供任何输入,则可以绕过 communicate 方法。

2其实子进程继承了进程的stdin、stdout、stderr,可能与Python的sys.stdinsys.stdout、@不匹配987654356@ 如果你已经覆盖了这些。这进入细节可能最好在这里忽略。 :-)

3我说“可能”而不是“将”,因为/dev/tty 访问控制终端,并不是所有的 pty 都是控制终端。这也变得复杂且特定于操作系统,也超出了此答案的范围。

【讨论】:

    【解决方案2】:

    尝试将--progress 选项添加到您的 git 命令中。这会强制 git 将进度状态发送到 stderr,即使 git 进程未连接到终端 - 这是通过 subprocess 函数运行 git 时的情况。

    git_cmd = "git clone --progress git@192.168.56.101:Mike_VonP/bit142_assign_2.git"
    
    print "SUBPROCESS.POPEN, COMMUNICATE" + "="*20
    p = subprocess.Popen(git_cmd.split(), stderr=subprocess.PIPE, stdout=subprocess.PIPE)
    tuple = p.communicate()
    p.wait()
    print "StdOut:\n" + tuple[0]
    print "StdErr:\n" + tuple[1]
    

    注意我无法在 Windows 上对此进行测试,但它在 Linux 上有效。

    此外,不必指定shell=True,这可能是一个安全问题,因此最好避免。

    【讨论】:

    • 非常感谢!这非常有效! (在 Windows 7 上的 Python 2.7 上测试)
    • 我删除了 shell=True。它似乎工作一样,所以我会把它拿出来。我正在编写的程序是由坐在命令提示符前的用户运行的,因此认为可以从命令提示符运行我的程序或从命令提示符运行任何他们想要的任何东西的用户的安全风险可以忽略不计。不过,如果摆脱它没有坏处,那么就没有必要保留它 - 谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-12-13
    • 2014-07-31
    • 2015-03-04
    • 1970-01-01
    • 2020-08-28
    • 1970-01-01
    • 2023-04-06
    相关资源
    最近更新 更多