我正在尝试从Mac OSX上的Python脚本控制less
。基本上我想要的是能够转发控制字符(上/下/左/右)但处理Python程序中的其他输入。我正在使用Popen启动less
,但less
reads user input from a source other than stdin
。因此,我不确定如何将任何字符发送到更少。
该程序打开较少,等待一秒,然后尝试使用两个单独的渠道发送q
退出:stdin
和/dev/tty
(因为它在SO问题中提到我以上链接)。两者都不起作用。
from subprocess import Popen, PIPE
import time
p1 = Popen("echo hello | less -K -R", stdin=PIPE, shell=True)
time.sleep(1)
p1.stdin.write(bytes('q', 'utf-8'))
with open("/dev/tty", 'w') as tty:
tty.write('q')
p1.wait()
如何从Python脚本控制less
?
答案 0 :(得分:2)
有一点涉及,但可以使用forkpty(3)
创建一个新的TTY,您可以完全控制less
,将输入和输出转发到原始TTY,使其感觉无缝。
下面的代码使用Python 3及其标准库。 pexpect可以做很多繁重的工作,但这不包含在Python中。而且这种方式更具教育意义。
import contextlib
import fcntl
import io
import os
import pty
import select
import signal
import struct
import termios
import time
import tty
假设其余代码缩进以在此上下文管理器中运行。
with contextlib.ExitStack() as stack:
我们需要抓住真正的TTY并将其设置为原始模式。这可能会混淆TTY的其他用户(例如,此程序退出后的shell),因此请务必将其恢复到相同的状态。
tty_fd = os.open('/dev/tty', os.O_RDWR | os.O_CLOEXEC)
stack.callback(os.close, tty_fd)
tc = termios.tcgetattr(tty_fd)
stack.callback(termios.tcsetattr, tty_fd, termios.TCSANOW, tc)
tty.setraw(tty_fd, when=termios.TCSANOW)
然后我们可以调用forkpty
,它在Python中名为pty.fork()
。这有几件事:
孩子应该跑less
。请注意使用_exit(2)
,因为在fork
之后继续执行其他代码可能不安全。
child_pid, master_fd = pty.fork()
if child_pid == 0:
os.execv('/bin/sh', ('/bin/sh', '-c', 'echo hello | less -K -R'))
os._exit(0)
stack.callback(os.close, master_fd)
然后设置一些异步信号处理程序需要做一些工作。
SIGCHLD
。我们可以用它来跟踪孩子是否还在跑步。SIGWINCH
。我们将此大小转发给PTY(它会自动向附加到它的进程发送另一个窗口更改信号)。我们应该将PTY的窗口大小设置为与start一致。转发SIGINT
,SIGTERM
等信号也是有意义的。
child_is_running = True
def handle_chld(signum, frame):
while True:
pid, status = os.waitpid(-1, os.P_NOWAIT)
if not pid:
break
if pid == child_pid:
child_is_running = False
def handle_winch(signum, frame):
tc = struct.pack('HHHH', 0, 0, 0, 0)
tc = fcntl.ioctl(tty_fd, termios.TIOCGWINSZ, tc)
fcntl.ioctl(master_fd, termios.TIOCSWINSZ, tc)
handler = signal.signal(signal.SIGCHLD, handle_chld)
stack.callback(signal.signal, signal.SIGCHLD, handler)
handler = signal.signal(signal.SIGWINCH, handle_winch)
stack.callback(signal.signal, signal.SIGWINCH, handler)
handle_winch(0, None)
现在真正的肉:在真实和假TTY之间复制数据。
target_time = time.clock_gettime(time.CLOCK_MONOTONIC_RAW) + 1
has_sent_q = False
with contextlib.suppress(OSError):
while child_is_running:
now = time.clock_gettime(time.CLOCK_MONOTONIC_RAW)
if now < target_time:
timeout = target_time - now
else:
timeout = None
if not has_sent_q:
os.write(master_fd, b'q')
has_sent_q = True
rfds, wfds, xfds = select.select((tty_fd, master_fd), (), (), timeout)
if tty_fd in rfds:
data = os.read(tty_fd, io.DEFAULT_BUFFER_SIZE)
os.write(master_fd, data)
if master_fd in rfds:
data = os.read(master_fd, io.DEFAULT_BUFFER_SIZE)
os.write(tty_fd, data)
它看起来很简单,虽然我正在掩饰一些事情,例如正确的短写和SIGTTIN
/ SIGTTOU
处理(部分通过抑制OSError
隐藏)。