【问题标题】:controlling less with Popen用 Popen 控制更少
【发布时间】:2018-06-29 18:29:20
【问题描述】:

我正在尝试通过 Mac OSX 上的 Python 脚本控制 less。基本上我想要的是能够转发控制字符(上/下/左/右)但在 Python 程序中处理其他输入。我使用Popen 启动less,但less reads user input from a source other than stdin。因此,我不确定如何将任何字符发送到 less。

程序打开较少,等待一秒钟,然后尝试使用两个单独的通道发送 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

【问题讨论】:

  • 这闻起来像XY Problem - 我能问一下为什么你想这样做吗?
  • @match 我想用它在命令行上显示字典条目,其中工作流应该是输入 word,ENTER -> 使用 less 显示条目 -> 输入 word,ENTER -> 显示与less... 我不想要求每次查找都少用“q”。 python-pager 太有限了,使用 less 的密钥文件功能退出除箭头之外的任何键仍然需要用户在输入下一个单词之前按某个键退出。如果有办法在less 中按下最后一个键,那么它也可以解决我的问题。
  • 所以你只想显示前 n 行一秒钟? head 会代替 less 工作吗?
  • @L3viathan 不,一秒钟的暂停和退出只是为了演示;我不希望less 立即退出,因为这样就很难判断它是否运行了。我实际上需要控制less 程序,或者其他合适的寻呼机。
  • 在原始模式下使用 tty 的程序不适用于管道。您需要使用 pty 与它进行交互,并使父级成为主/控制终端。

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


【解决方案1】:

这有点复杂,但可以使用 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的master端返回给原进程。

孩子应该跑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 的窗口大小以匹配启动。

转发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 部分隐藏)。

【讨论】:

  • 感谢您的撰写! “直截了当”,呵呵。我会尝试 pexpect 看看我能做什么。它还包含跨平台支持,这是一个加分项。
  • @NateGlenn 嘿,这有点深奥。 “直接”部分使用select 循环,但使用pty/termios 相对较少。 pexpect.spawn().interact() 的一些组合应该可以让你大部分时间到达那里,你只需要连接 WINCH 和其他一些你可以从这里复制的东西或 pexpect 源代码。祝你好运!
猜你喜欢
  • 2012-04-02
  • 2020-10-23
  • 2020-08-14
  • 2011-10-27
  • 1970-01-01
  • 2012-05-25
  • 2013-08-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多