如何捕获子进程的输入和输出? [英] How to capture inputs and outputs of a child process?

查看:109
本文介绍了如何捕获子进程的输入和输出?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试制作一个程序,该程序以可执行文件名作为参数,运行可执行文件并报告该运行的输入和输出.例如,考虑一个名为"circle"的子程序.我的程序需要运行以下内容:

I'm trying to make a program which takes an executable name as an argument, runs the executable and reports the inputs and outputs for that run. For example consider a child program named "circle". The following would be desired run for my program:


$ python3 capture_io.py ./circle
Enter radius of circle: 10
Area: 314.158997
[('output', 'Enter radius of circle: '), ('input',  '10\n'), ('output', 'Area: 314.158997\n')]

我决定为此任务使用 pexpect 模块.它具有一种称为 interact 的方法如上所示,它使用户可以与子程序进行交互.它还具有2个可选参数:output_filterinput_filter.从文档中:

I decided to use pexpect module for this job. It has a method called interact which lets the user interact with the child program as seen above. It also takes 2 optional parameters: output_filter and input_filter. From the documentation:

output_filter将传递子进程的所有输出. input_filter将通过用户的所有键盘输入.

The output_filter will be passed all the output from the child process. The input_filter will be passed all the keyboard input from the user.

这是我写的代码:

import sys
import pexpect

_stdios = []


def read(data):
    _stdios.append(("output", data.decode("utf8")))
    return data


def write(data):
    _stdios.append(("input", data.decode("utf8")))
    return data


def capture_io(argv):
    _stdios.clear()
    child = pexpect.spawn(argv)
    child.interact(input_filter=write, output_filter=read)
    child.wait()
    return _stdios


if __name__ == '__main__':
    stdios_of_child = capture_io(sys.argv[1:])
    print(stdios_of_child)

circle.c

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    float radius, area;

    printf("Enter radius of circle: ");
    scanf("%f", &radius);

    if (radius < 0) {
        fprintf(stderr, "Negative radius values are not allowed.\n");
        exit(1);
    }

    area = 3.14159 * radius * radius;
    printf("Area: %f\n", area);
    return 0;
}

哪个会产生以下输出:


$ python3 capture_io.py ./circle
Enter radius of circle: 10
Area: 314.158997
[('output', 'Enter radius of circle: '), ('input', '1'), ('output', '1'), ('input', '0'), ('output', '0'), ('input', '\r'), ('output', '\r\n'), ('output', 'Area: 314.158997\r\n')]

从输出中可以看到,输入被逐个字符地处理,并且回显为输出,从而造成混乱.是否可以更改此行为,以使我的input_filter仅在按下Enter时运行?

As you can observe from the output, input is processed character by character and also echoed back as output which creates such a mess. Is it possible to change this behaviour so that my input_filter will run only when Enter is pressed?

或更笼统地说,实现我的目标的最佳方法是什么(无论是否使用pexpect)?

Or more generally, what would be the best way to achieve my goal (with or without pexpect)?

推荐答案

是否可以更改此行为,使我的input_filter仅在按下Enter时运行?

,您可以通过从pexpect.spawn继承并覆盖interact方法来实现.我很快就会来.

Yes, you can do it by inheriting from pexpect.spawn and overwriting the interact method. I will come to that soon.

正如VPfB在他们的答案中指出的那样,您不能使用管道,我认为值得一提的是 pexpect的文档.

As VPfB pointed out in their answer, you can't use a pipe and I think it's worth to mentioning that this issue is also addressed in the pexpect's documentation.

您说的是

...输入被逐个字符处理,并作为输出回显...

... input is processed character by character and also echoed back as output ...

如果您检查 interact 您可以看到以下行:

If you examine the source code of the interact you can see this line:

tty.setraw(self.STDIN_FILENO)

这会将您的终端设置为原始模式:

This will set your terminal to raw mode:

输入可以逐个字符输入,...,并且禁止对终端输入和输出字符进行所有特殊处理.

input is available character by character, ..., and all special processing of terminal input and output characters is disabled.

这就是为什么input_filter函数在每次按键时都会运行,并且会看到退格键或其他特殊字符的原因.如果您可以注释掉这一行,则在运行程序时会看到以下内容:

That is why your input_filter function is running for every key press and it sees backspace or other special characters. If you could comment out this line, you would see something like this when you run your program:


$ python3 test.py ./circle
Enter radius of circle: 10
10
Area: 314.158997
[('output', 'Enter radius of circle: '), ('input', '10\n'), ('output', '10\r\n'), ('output', 'Area: 314.158997\r\n')]

这也可以让您编辑输入(即12[Backspace]0会给您相同的结果).但是正如您所看到的,它仍然会回显输入.可以通过为儿童终端设置一个简单标志来禁用此功能:

And this would also let you edit the input (i. e. 12[Backspace]0 would give you same result). But as you can see, it still echoes the input. This can be disabled by setting a simple flag for child's terminal:

mode = tty.tcgetattr(self.child_fd)
mode[3] &= ~termios.ECHO
tty.tcsetattr(self.child_fd, termios.TCSANOW, mode)

运行最新更改:


$ python3 test.py ./circle
Enter radius of circle: 10
Area: 314.158997
[('output', 'Enter radius of circle: '), ('input', '10\n'), ('output', 'Area: 314.158997\r\n')]

宾果!现在,您可以从pexpect.spawn继承并使用这些更改覆盖interact方法,或者使用Python的内置pty模块实现相同的操作:

Bingo! Now you can inherit from pexpect.spawn and override interact method with these changes or implement the same thing using the builtin pty module of Python:

import os
import pty
import sys
import termios
import tty

_stdios = []

def _read(fd):
    data = os.read(fd, 1024)
    _stdios.append(("output", data.decode("utf8")))
    return data


def _stdin_read(fd):
    data = os.read(fd, 1024)
    _stdios.append(("input", data.decode("utf8")))
    return data


def _spawn(argv):
    pid, master_fd = pty.fork()
    if pid == pty.CHILD:
        os.execlp(argv[0], *argv)

    mode = tty.tcgetattr(master_fd)
    mode[3] &= ~termios.ECHO
    tty.tcsetattr(master_fd, termios.TCSANOW, mode)

    try:
        pty._copy(master_fd, _read, _stdin_read)
    except OSError:
        pass

    os.close(master_fd)
    return os.waitpid(pid, 0)[1]


def capture_io_and_return_code(argv):
    _stdios.clear()
    return_code = _spawn(argv)
    return _stdios, return_code >> 8


if __name__ == '__main__':
    stdios, ret = capture_io_and_return_code(sys.argv[1:])
    print(stdios)

pexpect:

import sys
import termios
import tty
import pexpect

_stdios = []


def read(data):
    _stdios.append(("output", data.decode("utf8")))
    return data


def write(data):
    _stdios.append(("input", data.decode("utf8")))
    return data


class CustomSpawn(pexpect.spawn):
    def interact(self, escape_character=chr(29),
                 input_filter=None, output_filter=None):
        self.write_to_stdout(self.buffer)
        self.stdout.flush()
        self._buffer = self.buffer_type()
        mode = tty.tcgetattr(self.child_fd)
        mode[3] &= ~termios.ECHO
        tty.tcsetattr(self.child_fd, termios.TCSANOW, mode)
        if escape_character is not None and pexpect.PY3:
            escape_character = escape_character.encode('latin-1')
        self._spawn__interact_copy(escape_character, input_filter, output_filter)


def capture_io_and_return_code(argv):
    _stdios.clear()
    child = CustomSpawn(argv)
    child.interact(input_filter=write, output_filter=read)
    child.wait()
    return _stdios, child.status >> 8


if __name__ == '__main__':
    stdios, ret = capture_io_and_return_code(sys.argv[1:])
    print(stdios)

这篇关于如何捕获子进程的输入和输出?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆