是否有可能在 Python 中对标准输入进行两次连续的、成功的非阻塞读取?
Is it possible to do two consecutive, successful non-blocking reads of stdin in Python?
对长代码表示歉意post,但我相信它是有用的上下文。
我正在尝试解析原始 Python 中的特殊键(没有 curses),但似乎 select
进行非阻塞输入的技巧在这种情况下不起作用。特别是,看起来在读取输入的第一个字符后,select
返回的是 stdin
不可读,尽管还有更多的输入字符要读取。
重现问题的步骤:
- 运行下面的代码。
- 按左箭头键(或任何其他命名的特殊键)。
- 观察输出是
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))
对长代码表示歉意post,但我相信它是有用的上下文。
我正在尝试解析原始 Python 中的特殊键(没有 curses),但似乎 select
进行非阻塞输入的技巧在这种情况下不起作用。特别是,看起来在读取输入的第一个字符后,select
返回的是 stdin
不可读,尽管还有更多的输入字符要读取。
重现问题的步骤:
- 运行下面的代码。
- 按左箭头键(或任何其他命名的特殊键)。
- 观察输出是
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))