【问题标题】:Capture output as a tty in python在python中将输出捕获为tty
【发布时间】:2018-10-23 16:56:30
【问题描述】:

我有一个需要 tty(如标准输入和标准错误)的可执行文件,并且希望能够对其进行测试。 我想输入标准输入,并捕获标准输出和标准错误的输出,这是一个示例脚本:

# test.py
import sys
print("stdin: {}".format(sys.stdin.isatty()))
print("stdout: {}".format(sys.stdout.isatty()))
print("stderr: {}".format(sys.stderr.isatty()))
sys.stdout.flush()
line = sys.stdin.readline()
sys.stderr.write("read from stdin: {}".format(line))
sys.stderr.flush()

我可以在没有 tty 的情况下运行它,但这会被 .isatty 捕获并且每个都返回 False:

import subprocess
p = subprocess.Popen(["python", "test.py"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
p.stdin.write(b"abc\n")
print(p.communicate())
# (b'stdin: False\nstdout: False\nstderr: False\n', b'read from stdin: abc\n')

我想捕获 stdout 和 stderr 并让这三个都返回 True - 作为 tty。

我可以使用pty 来制作一个 tty 标准输入:

import subprocess
m, s = pty.openpty()
p = subprocess.Popen(["python", "test.py"], stdin=s, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdin = os.fdopen(m, 'wb', 0)
os.close(s)
stdin.write(b"abc\n")
(stdout, stderr) = p.communicate()
stdin.close()
print((stdout, stderr))
# (b'stdin: True\nstdout: False\nstderr: False\n', b'read from stdin: abc\n')

我尝试了一堆排列来使 stdout 和 stderr tty 无济于事。
我想要的输出是:

(b'stdin: True\nstdout: True\nstderr: True\n', b'read from stdin: abc\n')

【问题讨论】:

  • 我还没有测试,但是stdin=s, stdout=s, stderr=s呢?
  • @Sraw 这不会捕获 stdout 或 stderr 的输出。但是很可能有一个使用它的解决方案......
  • @Sraw 我的直觉是这需要 3 个 pty.openpty() 对,每个 stdin/stdout/stderr 一个,并以特定顺序写入、读取和关闭。我尝试了很多排列都无济于事。
  • @unutbu 你能粘贴这个作为答案吗?我很难看到它。也许我没有喝足够的咖啡......

标签: python subprocess popen tty


【解决方案1】:

下面的代码基于 jfs 的回答 herehere,加上您使用 3 个伪终端来区分 stdout、stderr 和 stdin 的想法(尽管注意有一个 cryptic warning 可能会出错(例如 OSX 上可能截断的标准错误?)。

另请注意,文档说 ptyfor Linux-only 尽管它“应该适用于”其他平台:

import errno
import os
import pty
import select
import subprocess

def tty_capture(cmd, bytes_input):
    """Capture the output of cmd with bytes_input to stdin,
    with stdin, stdout and stderr as TTYs.

    Based on Andy Hayden's gist:
    https://gist.github.com/hayd/4f46a68fc697ba8888a7b517a414583e
    """
    mo, so = pty.openpty()  # provide tty to enable line-buffering
    me, se = pty.openpty()  
    mi, si = pty.openpty()  

    p = subprocess.Popen(
        cmd,
        bufsize=1, stdin=si, stdout=so, stderr=se, 
        close_fds=True)
    for fd in [so, se, si]:
        os.close(fd)
    os.write(mi, bytes_input)

    timeout = 0.04  # seconds
    readable = [mo, me]
    result = {mo: b'', me: b''}
    try:
        while readable:
            ready, _, _ = select.select(readable, [], [], timeout)
            for fd in ready:
                try:
                    data = os.read(fd, 512)
                except OSError as e:
                    if e.errno != errno.EIO:
                        raise
                    # EIO means EOF on some systems
                    readable.remove(fd)
                else:
                    if not data: # EOF
                        readable.remove(fd)
                    result[fd] += data

    finally:
        for fd in [mo, me, mi]:
            os.close(fd)
        if p.poll() is None:
            p.kill()
        p.wait()

    return result[mo], result[me]

out, err = tty_capture(["python", "test.py"], b"abc\n")
print((out, err))

产量

(b'stdin: True\r\nstdout: True\r\nstderr: True\r\n', b'read from stdin: abc\r\n')

【讨论】:

  • 注意:这在 OSX 上对我有用......我猜这只是 python 核心团队“未经测试”。
  • 我知道这已经很老了,但它在 Windows 上不起作用,因为 pty 导入 tty 导入 termios 并且在 Windows 上没有 termios