Simultaneous Standard Input and Interactive Input

Last modified: 2011-12-08

Some time ago I had this problem: make a command line program that can simultaneously read input data from the standard input and read the keyboard to provide user interaction. If you've never done it before, it actually is more complicated than it seems. The problem is that user input is also read from the standard input, and user input and input data shouldn't interfere with each other. In some cases this could be avoided for instance by providing command line switch --assume-yes, but if the user wants to occasionally answer ‘no’ it won't work.

So I started looking for a solution. After searching it turned out that the well-known Unix program less does exactly what I wanted. It reads the text to be displayed from the standard input, and still lets the user scroll the text using keyboard. It's also clear that less can't read the whole input text first, and then read user input afterwards. That would be vastly inefficient. By reading the source code of less I figured out how to accomplish what I wanted.

The solution is to read user input from another source than the standard input. Different flavours of Unix provide a special file /dev/tty. User input can be read by opening and reading this file. That still isn't enough to solve the proposed problem because standard functions for reading user input read from the standard input. So the standard input should be replaced by the input stream coming from /dev/tty and the original input should be efficiently copied elsewhere. Luckily Unix lets programs to duplicate the file descriptors of open files. Thus the solution is to duplicate the original input stream to another file descriptor, and let file descriptor 0 to point at /dev/tty. File descriptor 0 is the file descriptor of the standard input, so functions which read user input read from the file descriptor 0. Now that the file descriptor 0 points to /dev/tty the program can read the user's input while still having the original input elsewhere.

Below a minimalistic Python program is provided which solves the problem. Experiment with it. Note that this example was written for Python 3. It is trivial to convert it to work with Python 2.*.


#!/usr/bin/python3
# -*- coding: utf-8 -*-

import os

# Duplicates the standard input into file descriptor 3.
# Note that if some file already had file descriptor 3,
# it will be closed. Avoid this somehow if needed.
os.dup2(0, 3)
# Opens a TTY for reading the keyboard.
tty = open("/dev/tty", mode="r")
# Duplicates the TTY into file descriptor 0 making the
# keyboard input standard input.
os.dup2(tty.fileno(), 0)
# Opens the original duplicated standard input into newInput.
newInput = os.fdopen(3, "r")

# Gets some input from the user.
ans = input("Type something: ")

print("You typed: '{}'.".format(ans))
print("Standard input given: " + str(newInput.readlines()))

# Obviously TTY should be closed after usage.
tty.close()

Now run the command below in your shell and provide ‘Hello world!\n’ as input when the program (saved as example.py) asks for it.


$ echo 'some important data to be processed' | ./example.py

This is the output:


Type something: Hello world!
You typed: 'Hello world!'.
Standard input given: ['some important data to be processed\n']

Well that was relatively easy after all. I hope this was useful and saved you some minutes and at least prevented the occasional headache.

Note on Consecutive Reads

The above code solves the more general case of simultaneous reads of both keyboard and standard input. However, if you only need to read the standard input and keyboard consecutively, then the code below suffices.


#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys

# Read the whole standard input into memory.
inputData = sys.stdin.read()
# Change sys.stdin to point at the stream from /dev/tty.
sys.stdin = open("/dev/tty", mode="r")

print("Standard input given:")
print(inputData)
answer = input("How did you like it?\n")
print("\nSeems that you typed '{}'".format(answer))

# Close the stream.
sys.stdin.close()