Python pty.spawn stdin没有回显,而是重定向到master的stdout

时间:2017-04-08 11:53:59

标签: python linux bash stdin pty

我想从Python调用一个程序,并使其相信即使Python的进程stdout附加到管道,它的stdout也是一个tty。所以我使用pty.spawn函数来实现这一点,可以通过以下方式验证:

$ python -c "import sys; from subprocess import call; call(sys.argv[1:])" python -c "import sys; print sys.stdout.isatty()" | cat
False

$ python -c "import sys; import pty; pty.spawn(sys.argv[1:])" python -c "import sys; print sys.stdout.isatty()" | cat
True

(我们看到在第二个命令中我们已经实现了我们的目标,即产生的进程被欺骗认为它的标准输出是tty。)

但问题是,如果我们使用pty.spawn,那么它的输入不会被回显,而是被重定向到主要的标准输出。这可以通过以下命令看到:

$ python -c "import sys; import pty; pty.spawn(sys.argv[1:])" cat > out.txt
$ # Typed "hello" in input, but that is not echoed (use ^D to exit). It is redirected to output.txt
$ cat out.txt
hello
hello

(但是当我们使用subprocess.call

时,这个问题不存在
$ python -c "import sys; from subprocess import call; call(sys.argv[1:])" cat > out1.txt
hello
$ cat out1.txt
hello

因为它的stdin和stdout正确地附加到主服务器上。)

我无法找到一种方法,以便Python调用一个程序,它将stdout视为tty(类似于pty.spawn),但其输入正确回显(类似于{{ 1}})。有什么想法吗?

1 个答案:

答案 0 :(得分:5)

您正在创建一个stdout连接到文件的终端,因此终端执行的正常回显将被发送到文件而不是屏幕。

我不确定spawn是否可以像这样直接使用:pty库提供pty.fork()来创建子进程并返回stdin / stdout的文件描述符。但是你需要更多的代码才能使用它。

要克服当前与spawn的问题,还有两个简单的选择:

选项1:如果您关心的是将生成的命令的输出发送到文件,那么您可以(我更喜欢命名管道和here文件用于python one-衬套):

$ python <(cat << EOF
import sys
import pty
print 'start to stdout only'
pty.spawn(sys.argv[1:])
print 'complete to stdout only'
EOF
) bash -c 'cat > out.txt'

在运行时会如下所示:

start to stdout only
hello
complete to stdout only

表示输入(我键入的hello)和print语句的结果将进入屏幕。 out.txt的内容将是:

$ cat out.txt
hello

即只是你输入的内容。

选项2:另一方面,如果你想让out文件包含生成的命令输出周围的python输出,那么你需要更复杂的东西,比如:

python <(cat << EOF
import sys
import pty
import os
old_stdout = sys.stdout
sys.stdout = myfdout = os.fdopen(4,"w")
print 'start to out file only'
myfdout.flush()
pty.spawn(sys.argv[1:])
print 'complete to out file only'
sys.stdout = old_stdout
EOF
) bash -c 'cat >&4' 4>out.txt

只有在运行时才输出到终端(即你输入的内容):

hello

但out文件将包含:

$ cat out.txt
start to out file only
hello
complete to out file only

后台: python pty库功能强大:它创建了一个连接到python的终端设备,处理stdout和stdin。我想大多数使用它将使用pty.fork()调用,以便真正的stdin / stdout不受影响。

但是在你的情况下,在你的shell中,你将python进程的stdout重定向到一个文件。因此,生成的pty也将其stdout附加到文件中,因此正在重定向将stdin回送到stdout的正常操作。常规标准输出(屏幕)仍然存在,但新pty没有使用。

上面选项1 的关键区别是将stdout的重定向移到pty.spawn调用内的某处,以便创建的pty仍然与实际终端有明确的连接stdout(用于在键入时尝试回显stdin)

选项2 的区别在于在任意文件描述符(即文件描述符4)上创建第二个通道,并使用它代替stdout,一旦进入python并创建了生成进程(即将生成的进程的stdout重定向到同一文件描述符)

这两个区别都阻止了pty.spawn创建的pty从其stdout更改或与真实终端断开连接的pty。这允许stdin的回显正常工作。

有些软件包使用pty库并为您提供更多控制权,但您会发现其中大部分都使用pty.fork()(有趣的是我还没找到一个实际使用{{1} }})

编辑以下是使用pty.fork()的示例:

pty.spawn

编辑questionimport sys import pty import os import select import time import tty import termios print 'start' try: pid, fd = pty.fork() print 'forked' except OSError as e: print e if pid == pty.CHILD: cmd = sys.argv[1] args = sys.argv[1:] print cmd, args os.execvp(cmd,args) else: tty.setraw(fd, termios.TCSANOW) try: child_file = os.fdopen(fd,'rw') read_list = [sys.stdin, child_file] while read_list: ready = select.select(read_list, [], [], 0.1)[0] if not ready and len(read_list) < 2: break elif not ready: time.sleep(1) else: for file in ready: try: line = file.readline() except IOError as e: print "Ignoring: ", e line = None if not line: read_list.remove(file) else: if file == sys.stdin: os.write(fd,line) else: print "from child:", line except KeyboardInterrupt: pass 提供了一些良好的链接

更新:应该在代码中添加一些注释 pty.fork()示例的工作原理:

当解释器执行对pty.fork()的调用时,处理分为两部分:现在有两个线程似乎都刚刚执行了pty.fork()调用。

一个线程是您最初的线程(父线程),一个是新线程(子线程)。

在父级中,pty.fork()pid设置为子级的进程ID,文件描述符连接到子级的stdin和stdout:在父级中,当您从fd读到你正在阅读写给孩子们的内容;当你写信给fd时,你正在给孩子们写信。所以现在,在父级中,我们可以通过其stdout / stdin与其他线程进行通信。

在孩子中,fd设置为0且未设置pid。如果我们想与父线程交谈,我们可以通过stdin / stdout读取和写入,知道父母可以而且应该对此做些什么。

这两个线程将从此时开始执行相同的代码,但我们可以根据fd中的值判断我们是在父线程还是子线程中。如果我们想在子线程和父线程中执行不同的操作,那么我们只需要一个条件语句,将子进程发送到一个代码路径,将父进程发送到不同的代码路径。这就是这条线的作用:

pid

在孩子中,我们只想在新的pty中生成新命令。使用if pid == pty.CHILD: #child thread will execute this code .... else #parent thread will execute this code ... 是因为我们将使用此方法更多地控制pty作为终端,但基本上与os.execvp fd pty.spawn()'. This means the child stdin/stdout are now connected to the command you wanted via a pty. IMmportantly, any input or output from the command (or the pty for that matter) will be available to the parent thread by reading from fd`

相同

所以现在,在父级中,我们需要通过读写. And the parent can write to the command via pty by writing to将真正的stdin / stdout连接到子stdin / stdout。这就是父代码现在做的事情(fd部分)。在真正的stdin上出现的任何数据都写到else。从fd(由父项)读取的任何数据都将写入stdout。所以父线程现在唯一要做的就是在真正的stdin / stdout和fd之间进行代理。如果你想以编程方式对命令的输入和输出做一些事情,那就是你要做的事情。

父母身上发生的另一件事是这个电话:

fd

这是告诉孩子的pty停止回声的一种方法。

这解决了您最初遇到的问题: - 您的本地终端仅连接到父线程 - 正常回显就绪(即在您的输入传递到流程之前) - 可以重定向该进程的标准输出 - 无论你使用终端stdout做什么都不会影响子进程的stdin / stdout - 已告知子进程不对其标准输入进行本地回显

这似乎有很多解释 - 如果有人为了清晰起见有任何修改?