Import Readline

By Susam Pal on 24 Feb 2022

Toy REPL

Let us first write a tiny Python program to create a toy read-eval-print-loop (REPL) that does only one thing: add all integers entered as input into the REPL prompt. Here is the program:

while True:
    try:
        line = input('> ')
        print(sum([int(n) for n in line.split()]))
    except ValueError as e:
        print('error:', e)
    except (KeyboardInterrupt, EOFError):
        break

Here is how it works:

$ python3 repl.py
> 10 20 30
60
> 40 50 60
150
> 

If we now type (the up arrow key) or ctrl+p to bring back the previous input, we see something like the following instead:

> ^[[A^[[A^P^P

It shows the keys typed literally rather than bringing up previous input like most other interactive programs with a command-line interface do. The other programs that do bring up the previous input are able to do so because they provide line editing and history capability, often with the help of a line editing and history library like GNU Readline (libreadline) or BSD Editline (libedit).

Can we have a similar line editing and history capability for our toy REPL? After all, the Python REPL itself offers such a line editing facility. Surely there must be a way to have this facility for our own programs too. Indeed there is!

Line Editing and History

To enable line editing and history in our toy REPL, we just need to add import readline to our program. Here is how our program would look:

import readline

while True:
    try:
        line = input('> ')
        print(sum([int(n) for n in line.split()]))
    except ValueError as e:
        print('error:', e)
    except (KeyboardInterrupt, EOFError):
        break

Now , ctrl+p, etc. work as expected.

$ python3 repl.py
> 10 20 30
60
> 40 50 60
150
> 40 50 60

The last line of input in the example above is obtained by typing either or ctrl+p. In fact, all of the line editing keys like ctrl+a to go to the beginning of the line, ctrl+k to kill the line after the cursor, etc. work as expected. The exact list of default key-bindings supported depends on the underlying line editing library being used by the readline module. The underlying library may be either the GNU Readline library or the BSD Editline library. There are some minor differences regarding the list of default key-bindings between these two libraries.

History File

What we have done so far achieves the goal of bringing up previous inputs from the history. However, it does not bring back inputs from a previous invocation of the REPL. For example, if we start our toy REPL, enter some inputs, then quit it (say, by typing ctrl+c), start our toy REPL again, and type or ctrl+p, it does not bring back the input from the previous invocation. For a full-blown REPL meant for sophisicated usage, we may want to preserve the history between different invocations of the REPL. This can be achieved by using the read_history_file() and write_history_file() functions as shown below:

import readline
import os

HISTORY_FILE = os.path.expanduser('~/.repl_history')
if os.path.exists(HISTORY_FILE):
    readline.read_history_file(HISTORY_FILE)

while True:
    try:
        line = input('> ')
        readline.write_history_file(HISTORY_FILE)
        print(sum([int(n) for n in line.split()]))
    except ValueError as e:
        print('error:', e)
    except (KeyboardInterrupt, EOFError):
        break

For more information on how to use this module, see the Python readline documentation.

Readline Wrapper

At this point, it is worth mentioning that there are many interactive CLI tools that do not have line editing and history capabilities. They behave like our first toy REPL example in this post. Fortunately, there is the wonderful readline wrapper utility known as rlwrap that can be used to enable line editing and history in such tools. This utility can often be easily installed from package repositories of various operating systems. Here is a demonstration of this tool:

$ rlwrap cat
hello
hello
world
world
world

The last line of input in the example above is obtained by typing either or ctrl+p. In the above example, the input history is automatically saved to ~/.cat_history, so it is possible to bring back inputs from a previous invocation of the command.

Obligatory Joke

Finally, an obligatory XKCD comic to conclude this post:

XKCD comic on Python
Python by Randall Munroe (Source: https://xkcd.com/353/)

While the days of achieving air flight with a single import statement might still be a few decades away, we do have the luxury to enable line editing and history in our REPLs with a single such statement right now.

Comments