Writing an interactive REPL in Python
Today I figured out how to write a repl prompt with history and editing in python. Normally when you are just using the built-in "raw_input" in a while loop, things don't work quite right. The arrow keys don't necessarily work as expected, and there are other problems. I looked at curses, and a couple other options but ended up writing my own solution.
To make it work, you need to execute a little magic. namely:
Figuring out the correct setting for the terminal took me a bit of time. But once you have the terminal setup like this the following things happen:
- input is no longer line buffered and is no longer auto echoed to the user
- This means you can process every character sent to the terminal in near real time
- More importantly since you get *every* character you can process special characters like the arrow keys which are actually multi-byte
- Since it isn't auto echoed you have to send the characters you want back to the terminal
- control characters are not printed to the terminal
- Meaning when you press [up arrow] you no longer see ^[A
- It also means the cursor in the terminal moves in response to the arrow key press
- you can programmatically move the cursor
- For instance to move the cursor left
Now writing the REPL is a matter of figuring out the control logic which goes inside the try block. Since REPL stands for Read, Eval, Print, Loop I started by writing an infinite while loop. In my loop you can exit either by using the exit command I defined in my command language or by typing control C. Pretty simple. the EVAL_LOGIC will depend on what language your implementing. Mine was simple, command [optional argument]. So my eval logic looked at the first word in the prompt determined if it was defined, if it was it executed the command. Obviously many languages are much more complex than this, mine was not so I won't go any further into the details on implementing EVAL_LOGIC instead I will talk about giving the READ LOGIC some nice features.
I wanted my REPL to have history, and I wanted the user to be able to edit the line by using the backspace key, and the arrow keys. Not complex features, but if you use raw_input() as your read logic they will not be there out of the box. By necessity the READ LOGIC processes each character as it entered by the user. To accomplish this I used an inner while 1 loop: Before I get into the nitty gritty the control key logic I am going to explain the function "clear_line" used in the above code. clear_line does exactly what one would think it would do, it clears the line, and replace the front of the line with whatever prompt is given by the user. Here is the code: The most interesting thing about this code is to delete characters in a terminal one moves the cursor left enters a space and then move the character left again. You should note in clear_line I assume the maximum size of the terminal is 150 characters. I am sure there is a way to get the actual size of you terminal from the environment, but for the first cut of my REPL I haven't bothered to look that up yet. The logic for control characters works in a similar way to clear_line. First it detects what character has been entered. Then if it is an up character it must move the cursor down, and replace the prompt with a previously entered line from the history. If it is down it clears the prompt. Left and right allow the cursor to move left and right only if it stays within the characters the user has entered. If they stray it move the cursor back. Finally backspace simply removes the previous character from the prompt. Thats it! All I needed to do to write an improved interactive REPL in Python. Hopefully this helps some one out there who is using raw_input (as I have done so many times before) and wanting a better solution. - Tim Henderson