用Popen控制较少

时间:2018-01-20 17:53:22

标签: python python-3.x unix subprocess less-unix

我正在尝试从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

1 个答案:

答案 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()。这有几件事:

  • 创建pseudoterminal
  • 分叉一个新的孩子。
  • 将孩子附加到PTY的从属端。
  • 将孩子的PID和PTY的主端返回原始过程。

孩子应该跑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一致。

转发SIGINTSIGTERM等信号也是有意义的。

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隐藏)。