How to check potentially empty stdin without waiting for input?

Question:

I am attempting to read from keyboard input without waiting for input. The purpose is to be used in an "infinite" loop aka while True:.

Thus far I’ve been trying to manipulate the readchar library https://pypi.python.org/pypi/readchar/0.6 , but with no luck. While it doesn’t wait for Enter, it still waits for some input. I don’t want it to wait for input, but simply to check and return "" or some placeholder if there is no input.

Here is what I’ve been working with:

def readchar():
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    return ch

def main():
    while True:
        current = readchar()
        if current == "some letter":
            print("things happen")
Asked By: LegendaryQ

||

Answers:

The POSIX I/O functions that ultimately underlie Python’s file objects have two different modes, blocking and non-blocking, controlled by a flag named O_NONBLOCK. In particular, the read function says:

When attempting to read a file … that … has no data currently available … If O_NONBLOCK is set, read() shall return -1 and set errno to [EAGAIN].

In Python, this flag is available in the os module.


sys.stdin is already open, so you can’t just pass O_NONBLOCK to the os.open function, so… what do you do? Well, you actually might want to open /dev/tty instead; it kind of depends on what you’re actually doing. In that case, the answer is obvious. But let’s assume that you don’t. So, you want to change the flags of the already-open file. That’s exactly what fcntl is for. You use the F_GETFL operation to read the current flags, or in the bit for O_NONBLOCK, and F_SETFL the result. You can remember the current flags for later if you want to restore things, of course.

In Python, the fcntl function, and the operation constants, are available in the fcntl module.


One last problem: sys.stdin isn’t a raw file object, it’s a TextIOWrapper that does Unicode decoding on top of a BufferedReader, which itself adds buffering on top of a FileIO. So, sys.stdin.read() isn’t directly calling the POSIX read function. In order to do that, you need to use sys.stdin.buffer.raw. And you may also need to do lots of careful flushing if you want to go back and forth between raw and normal input. (Note that this means you’re giving up Unicode and instead getting a single-byte bytes object, which could be, say, half of a UTF-8 character or a quarter of a terminal escape character. Hopefully you’re expecting that.)


Now, what does FileIO.read return when nothing is available? Well, it’s an implementation of RawIOBase, and RawIOBase.read says:

If 0 bytes are returned, and size was not 0, this indicates end of file. If the object is in non-blocking mode and no bytes are available, None is returned.

In other words, you’re going to get None if nothing is available, b'' for EOF, or a single-byte bytes for anything else.


So, putting it all together:

old_settings = termios.tcgetattr(fd)
old_flags = fcntl.fcntl(fd, fcntl.F_GETFL)
try:
    tty.setraw(fd)
    fcntl.fcntl(fd, fcntl.F_SETFL, old_flags | os.O_NONBLOCK)
    return sys.stdin.buffer.raw.read(1)
finally:
    fcntl.fcntl(fd, fcntl.F_SETFL, old_flags)
    termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

One last thing: Is there a way to tell if input is ready without reading it? Yes, but not portably. Depending on your platform, select, poll, epoll, and/or kqueue may be available and may work on regular files. read(0) may be guaranteed to return b'' instead of None. And so on. You can read your local man pages. But a simpler solution is the same thing C’s stdio does: add a 1-byte-max buffer, and use it to implement your own read and peek or read and unread wrappers.

Answered By: abarnert
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.