是否有可能在 Python 中对标准输入进行两次连续的、成功的非阻塞读取?

Is it possible to do two consecutive, successful non-blocking reads of stdin in Python?

对长代码表示歉意post,但我相信它是有用的上下文。

我正在尝试解析原始 Python 中的特殊键(没有 curses),但似乎 select 进行非阻塞输入的技巧在这种情况下不起作用。特别是,看起来在读取输入的第一个字符后,select 返回的是 stdin 不可读,尽管还有更多的输入字符要读取。

重现问题的步骤:

  1. 运行下面的代码。
  2. 按左箭头键(或任何其他命名的特殊键)。
  3. 观察输出是 ESC 后跟各行中的转义序列的其余部分。预期行为:输出 ARROW_LEFT.

是否可以正确读取特殊键的完整转义序列,同时仍能正确读取 ESC 本身?

#!/usr/bin/env python3

import sys
from enum import Enum
import tty
import termios
import select
import signal

# Takes a given single-character string and returns the string control version
# of it. For example, it takes 'c' and returns the string representation of
# Control-C.  This can be used to check for control-x keys in the output of
# readKey.
def controlKey(c):
  return chr(ord(c) & 0x1f)

def nonblock_read(stream, limit=1):
  if select.select([stream,],[],[],0.1)[0]:
    return stream.read(limit)
  return None

# Read a key of input as a string. For special keys, it returns a
# representative string. For control keys, it returns the raw string.
# This function assumes that the caller has already put the terminal in raw mode.
def readKey():
  c = nonblock_read(sys.stdin, 1)
  if not c: return None
  # Handle special keys represented by escape sequences
  if c == "\x1b":
    seq = [None] * 3
    seq[0] = nonblock_read(sys.stdin, 1)
    if not seq[0]: return "ESC"
    seq[1] = nonblock_read(sys.stdin, 1)
    if not seq[1]: return "ESC"

    if seq[0] == '[':
      if seq[1] >= '0' and seq[1] <= '9':
        seq[2] = nonblock_read(sys.stdin, 1)
        if not seq[2]: return "ESC"

        if seq[2] == '~':
          if seq[1] == '1': return "HOME_KEY"
          if seq[1] == '3': return "DEL_KEY"
          if seq[1] == '4': return "END_KEY"
          if seq[1] == '5': return "PAGE_UP"
          if seq[1] == '6': return "PAGE_DOWN"
          if seq[1] == '7': return "HOME_KEY"
          if seq[1] == '8': return "END_KEY"
      else:
        if seq[1] == 'A': return "ARROW_UP"
        if seq[1] == 'B': return "ARROW_DOWN"
        if seq[1] == 'C': return "ARROW_RIGHT"
        if seq[1] == 'D': return "ARROW_LEFT"
        if seq[1] == 'H': return "HOME_KEY"
        if seq[1] == 'F': return "END_KEY"
    elif seq[0] == 'O':
      if seq[1] == 'H': return "HOME_KEY"
      if seq[1] == 'F': return "END_KEY"
    return 'ESC'
  return c

def main():
  # Save terminal settings
  fd = sys.stdin.fileno()
  old_tty_settings = termios.tcgetattr(fd)
  # Enter raw mode
  tty.setraw(sys.stdin)
  ################################################################################  
  interrupt = controlKey("c")
  while True:
    s = readKey()
    if s:
      print(f"{s}", end="\r\n")
    if s == interrupt:
      break
  ################################################################################  
  # Exit raw mode
  fd = sys.stdin.fileno()
  termios.tcsetattr(fd, termios.TCSADRAIN, old_tty_settings)

if __name__ == "__main__":
  main()

如果你使用低级 I/O,我认为它有效。 select.select 将接受数字文件描述符。我没有尝试将它与您的程序集成,但可以尝试一下。如果你按例如,你应该得到一个字符序列左箭头。原始版本似乎不适用于 sys.stdin,但这适用于 fd 0。请注意 os.read 从数字文件描述符中读取。

import os
import sys
import select
import tty
import termios

def read_all_available(fd):
    "do a single blocking read plus non-blocking reads while any more data exists"
    if not select.select([fd],[],[], None)[0]:
        return None
    val = os.read(fd, 1)
    while select.select([fd],[],[], 0)[0]:
        val += os.read(fd, 1)
    return val


data = None
while data != b'\x03':
    old_settings = termios.tcgetattr(0)
    tty.setraw(sys.stdin)
    data = read_all_available(0)

    # reset settings here just to allow tidier printing to screen
    termios.tcsetattr(0, termios.TCSADRAIN, old_settings)
    print(data, len(data))