运行 在哑终端中使用 Python subprocess.Popen 和 pty 进行交互 Bash
Run interactive Bash in dumb terminal using Python subprocess.Popen and pty
这个问题类似于,除了我想在“哑”终端(TERM=dumb
)中运行 Bash,并且不把tty进入原始模式。
下面的代码是我的尝试。该代码类似于链接问题中给出的解决方案,主要区别在于它没有将 tty 置于原始模式,并设置 TERM=dumb
.
import os
import pty
import select
import subprocess
import sys
master_fd, slave_fd = pty.openpty()
p = subprocess.Popen(['bash'],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
# Run in a new process group to enable bash's job control.
preexec_fn=os.setsid,
# Run bash in "dumb" terminal.
env=dict(os.environ, TERM='dumb'))
while p.poll() is None:
r, w, e = select.select([sys.stdin, master_fd], [], [])
if sys.stdin in r:
user_input = os.read(sys.stdin.fileno(), 10240)
os.write(master_fd, user_input)
elif master_fd in r:
output = os.read(master_fd, 10240)
os.write(sys.stdout.fileno(), output)
上面的代码有两个问题:
- 代码将重新回应用户输入的任何内容。例如,如果用户输入
printf ''
,上面的代码将在打印下一个 bash 提示之前在下一行打印 printf ''
。
- Ctrlc 和 Ctrld在
bash
. 中表现得不像人们所期望的那样
我该如何解决这些问题?
这正是不将 tty 置于 raw 模式的副作用。通常处理 pty 的程序(如 expect)会将外部 tty 置于 raw 模式。
您的 Python 脚本的 tty(或 pty)回显您输入的内容和新的 pty 第二次回显。您可以在新的 pty 上禁用 ECHO。例如:
$ python3 using-pty.py
bash-5.1$ echo hello
echo hello
hello
bash-5.1$ stty -echo
stty -echo
bash-5.1$ echo hello # <-- no double echo any more
hello
bash-5.1$ exit
exit
您的 Python 脚本的 tty 不在 raw 模式下,因此当您按 ctrl-d Python 不会得到文字 ctrl-d ('[=13=]4'
)。相反,Python 将达到 EOF,而 read()
returns 将达到一个空字符串。因此,要使生成的 shell 退出,您可以
user_input = os.read(sys.stdin.fileno(), 10240)
if not user_input:
# explicitly send ctrl-d to the spawned process
os.write(master_fd, b'')
else:
os.write(master_fd, user_input)
同样,Python 的 tty 不在 raw 模式下,所以当您按 ctrl-c,它不会得到文字 ctrl-c ('[=15=]3'
)。相反,它被杀死了。作为解决方法,您可以捕获 SIGINT
.
def handle_sigint(signum, stack):
global master_fd
# send ctrl-c
os.write(master_fd, b'')
signal.signal(signal.SIGINT, handle_sigint)
这个问题类似于TERM=dumb
)中运行 Bash,并且不把tty进入原始模式。
下面的代码是我的尝试。该代码类似于链接问题中给出的解决方案,主要区别在于它没有将 tty 置于原始模式,并设置 TERM=dumb
.
import os
import pty
import select
import subprocess
import sys
master_fd, slave_fd = pty.openpty()
p = subprocess.Popen(['bash'],
stdin=slave_fd,
stdout=slave_fd,
stderr=slave_fd,
# Run in a new process group to enable bash's job control.
preexec_fn=os.setsid,
# Run bash in "dumb" terminal.
env=dict(os.environ, TERM='dumb'))
while p.poll() is None:
r, w, e = select.select([sys.stdin, master_fd], [], [])
if sys.stdin in r:
user_input = os.read(sys.stdin.fileno(), 10240)
os.write(master_fd, user_input)
elif master_fd in r:
output = os.read(master_fd, 10240)
os.write(sys.stdout.fileno(), output)
上面的代码有两个问题:
- 代码将重新回应用户输入的任何内容。例如,如果用户输入
printf ''
,上面的代码将在打印下一个 bash 提示之前在下一行打印printf ''
。 - Ctrlc 和 Ctrld在
bash
. 中表现得不像人们所期望的那样
我该如何解决这些问题?
这正是不将 tty 置于 raw 模式的副作用。通常处理 pty 的程序(如 expect)会将外部 tty 置于 raw 模式。
您的 Python 脚本的 tty(或 pty)回显您输入的内容和新的 pty 第二次回显。您可以在新的 pty 上禁用 ECHO。例如:
$ python3 using-pty.py bash-5.1$ echo hello echo hello hello bash-5.1$ stty -echo stty -echo bash-5.1$ echo hello # <-- no double echo any more hello bash-5.1$ exit exit
您的 Python 脚本的 tty 不在 raw 模式下,因此当您按 ctrl-d Python 不会得到文字 ctrl-d (
'[=13=]4'
)。相反,Python 将达到 EOF,而read()
returns 将达到一个空字符串。因此,要使生成的 shell 退出,您可以user_input = os.read(sys.stdin.fileno(), 10240) if not user_input: # explicitly send ctrl-d to the spawned process os.write(master_fd, b'') else: os.write(master_fd, user_input)
同样,Python 的 tty 不在 raw 模式下,所以当您按 ctrl-c,它不会得到文字 ctrl-c (
'[=15=]3'
)。相反,它被杀死了。作为解决方法,您可以捕获SIGINT
.def handle_sigint(signum, stack): global master_fd # send ctrl-c os.write(master_fd, b'') signal.signal(signal.SIGINT, handle_sigint)