需要逐个字符的键盘输入,它可以与paste和ANSI转义序列进行良好的交互

时间:2015-01-02 23:44:57

标签: python unix select

我的程序("TRAC Processor")使用逐个字符的输入。我正在为字符串实现类似readline的输入功能,这些字符串以enter以外的字符(通常为')终止,并且可能本身是多行的。所以我在输入字符之间输出终端转义序列,包括查询终端仿真器的转义序列(光标位置和屏幕大小)。为了进行跨平台的单字符输入,我使用了http://code.activestate.com/recipes/134892/,这非常有帮助。

这与粘贴工作正常......直到我需要在粘贴的第一个字符后得到终端响应。似乎粘贴的文本与转义序列的响应混合在一起。我想我会在启动转义序列查询之前通过刷新输入缓冲区来修复它:等待10ms,如果没有输入继续;如果有输入,请缓冲它并再次等待直到没有输入。基于this post,我尝试使用select()轮询stdin。好主意,但它没有用,它产生了非常奇怪的行为。我在这个问题的原始版本中发布了这种奇怪的行为,认为我误解了选择并且有办法解决它。似乎没有,但我找到了另一种方法来刷新(并保存)输入流。我决定保留这个问题,并将该方法作为答案发布。

解释select()的问题here。在粘贴的第一个字符之后,其他字符已经被缓冲,并且select仅在已经缓冲的新输入之后才返回新输入。我无法让自己删除我所产生的MWE这种行为,所以你可以在下面看到它。

不幸的是,该帖子中提出的答案要么对我不起作用,要么需要更多解释。 @slowdog建议使用无缓冲的输入(os.read(stdin.fileno(),1)而不是stdin.read(1))。这解决了选择问题,但它破坏了粘贴:似乎第一个之后粘贴的所有字符都被缓冲,无论什么,所以你永远不会看到它们。它似乎也没有与转义序列响应很好地工作,它们似乎也得到了缓冲。它也很烦人,因为你需要刷新输出缓冲区,但这并不是那么糟糕。 @Omnafarious在一篇评论中说:“虽然,处理Python缓冲问题的另一种方法是简单地进行无参数读取,这应该读取当前可用的所有内容。”这最终是我所做的,如下所示,但“简单地”结果并非如此简单。还有另一个解决方案here,但我认为必须是一种无需线程即可实现此目的的方法。

顺便提一下,有一个相对简单的解决方法,因为事实证明,粘贴不是随机散布的对逃逸序列的响应。在转义序列响应之前读取整个粘贴的剩余部分,这样当您查找转义序列响应(它本身以转义开始)时,您可以只缓冲在转义之前读取的所有字符,并处理他们以后。如果您可能在终端上键入ESC字符,则此操作仅会失败。在任何情况下,到目前为止我都非常喜欢解决这个问题,我认为其他人可能会觉得答案很有价值。

无论如何,这里的FWIW是select问题的MWE,它只是回应文本而不是缓冲它:

def flush():
    import sys, tty, termios
    from select import select
    tty.setraw(sys.stdin.fileno())
    while True:
        rlist, wlist, xlist = select([sys.stdin], [], [], 1)
        if rlist == []: return
        sys.stdout.write(sys.stdin.read(1))

将其粘贴到Python提示符(2.7.9)中,并在末尾添加另一个空行。如果您调用flush()并输入一些文本的速度超过每秒一个字母,则会将其键入给您。例如,我键入“hello”然后暂停,得到了这个结果:

>>> flush()
hello>>> 

在OSX终端应用程序中(至少),如果你将单词text复制到剪贴板,调用该函数并在一秒内点击粘贴,这是你得到的:

>>> flush()
t>>> 

奇!只有第一个字母。再试一次,什么都不输入:

>>> flush()
>>> 

暂停一秒钟,什么都不做,就像没有输入等等,对吗?再试一次,点击?

>>> flush()
ext?>>> 

你可以在?之前获得剩余的粘贴,保存起来!另外,奇怪的是,在键入我不理解的?之前有1秒的暂停。如果你在这一点上再试一次,它就像正常一样。

好的,让我们再试一次,首先粘贴text,然后粘贴WTF,然后输入!

>>> flush()
t>>> flush()
extW>>> flush()
TF!>>> 

因此,粘贴仅提供第一个字母,并将其他字符保留在输入缓冲区中,并在W!之前暂停一秒。还有一件奇怪的事情:缓冲的字符不会在Python >>>提示符下输入。

一个挥之不去的问题:为什么在下一个字母回显之前你会得到额外的1秒暂停?选择并不总是等待整个时间段......

1 个答案:

答案 0 :(得分:0)

"无参数读取"通常被认为是一种读取所有可用字节的方法,这听起来非常适合这个应用程序。不幸的是,当你查看documentation时,read()与readall()相同,它会阻塞直到EOF。所以你需要将stdin设置为非阻塞模式。

一旦你这样做,你就会开始:

IOError: [Errno 35] Resource temporarily unavailable

当你谷歌这一点时,绝大多数回答说这个问题的解决方案是摆脱非阻塞模式...所以这里没有帮助。但是,this post解释说这只是在没有要返回的字符时非阻塞read()所做的事情。

这是我的flush()函数,其中大部分都是从该帖子中复制的:

def flush():
    import sys, tty, termios, fcntl, os
    fd = sys.stdin.fileno()
    old_attr = termios.tcgetattr(fd)
    old_fl = fcntl.fcntl(fd, fcntl.F_GETFL)
    try:
        tty.setraw(fd)
        fcntl.fcntl(fd, fcntl.F_SETFL, old_fl | os.O_NONBLOCK)
        inp = sys.stdin.read()
    except IOError, ex1:  #if no chars available generates exception
        try: #need to catch correct exception
            errno = ex1.args[0] #if args not sequence get TypeError
            if errno == 35:
                return '' #No characters available
            else:
                raise #re-raise exception ex1
        except TypeError, ex2:  #catch args[0] mismatch above
            raise ex1 #ignore TypeError, re-raise exception ex1
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_attr)
        fcntl.fcntl(fd, fcntl.F_SETFL, old_fl)
    return inp

希望它对某人有帮助!