4 Python libraries for building great command-line user interfaces

In the second installment of a two-part series on terminal applications with great command-line UIs, we explore Prompt Toolkit, Click, Pygments, and Fuzzy Finder.
728 readers like this.
Getting started with 4 practical Python libraries: Prompt Toolkit, Click, Pygments, and Fuzzy Finder

Mennonite Church USA Archives. Modified by Opensource.com. CC BY-SA 4.0

This is the second installment in my two-part series on terminal applications with great command-line UIs. In the first article, I discussed features that make a command-line application a pure joy to use. In part two, I'll look at how to implement those features in Python with the help of a few libraries. By the end of this article, readers should have a good understanding of how to use Prompt Toolkit, Click (Command Line Interface Creation Kit), Pygments, and Fuzzy Finder to implement an easy-to-use REPL.

I plan to achieve this in fewer than 20 lines of Python code. Let's begin.

Python Prompt Toolkit

I like to think of this library as the Swiss Army knife of command-line apps—it acts as a replacement for readline, curses, and much more. Let's install the library and get started:

pip install prompt_toolkit

We'll start with a simple REPL. Typically a REPL will accept user input, do an operation, and print the results. For our example, we're going to build an "echo" REPL. It merely prints back what the user typed in:

REPL

from prompt_toolkit import prompt

while 1:
    user_input = prompt('>')
    print(user_input)

That is all it takes to implement a REPL. It can read user input and print out what they have entered. The prompt function used in this code snippet is from the prompt_toolkit library; it is a replacement for the readline library.

History

To enhance our REPL, we can add command history:


from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory

while 1:
    user_input = prompt('>', 
                        history=FileHistory('history.txt'),
                       )
    print(user_input)

We've just added persistent history to our REPL. Now we can use the up/down arrow to navigate the history, and use the Ctrl+R to search the history. This satisfies the basic etiquette of a command line.

Auto-suggestion

One of the discoverability tricks I covered in part one was the automatic suggestion of commands from the history. (We saw this feature pioneered in the fish shell.) Let's add that feature to our REPL:


from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory

while 1:
    user_input = prompt('>', 
                        history=FileHistory('history.txt'),
                        auto_suggest=AutoSuggestFromHistory(),
                       )
    print(user_input)

All we had to do was add a new argument to the prompt() API call. Now we have a REPL that has fish-style auto-suggestion from the history.

Auto-completion

Now let's implement an enhancement of Tab-completion via auto-completion, which pops up possible suggestions as the user starts typing input.

How will our REPL know what to suggest? We supply a dictionary of possible items to suggest.

Let's say we're implementing a REPL for SQL. We can stock our auto-completion dictionary with SQL keywords. Let's see how to do that:

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.contrib.completers import WordCompleter

SQLCompleter = WordCompleter(['select', 'from', 'insert', 'update', 'delete', 'drop'],
                             ignore_case=True)

while 1:
    user_input = prompt('SQL>', 
                        history=FileHistory('history.txt'),
                        auto_suggest=AutoSuggestFromHistory(),
                        completer=SQLCompleter,
                        )
    print(user_input)

Once again, we simply can use a built-in completion routine of prompt-toolkit called WordCompleter, which matches the user input with the dictionary of possible suggestions and offers up a list.

We now have a REPL that can do auto-completion, fish-style suggestions from history, and up/down traversal of history. All of that in less than 10 lines of actual code.

Click

Click is a command-line creation toolkit that makes it easy to parse command-line options arguments and parameters for the program. This section does not talk about how to use Click as an arguments parser; instead, I'm going to look at some utilities that ship with Click.

Installing click is simple:

pip install click

Pager

Pagers are Unix utilities that display long output one page at a time. Examples of pagers are less, more, most, etc. Displaying the output of a command via a pager is not just friendly design, but also the decent thing to do.

Let's take the previous example further. Instead of using the default print() statement, we can use click.echo_via_pager(). This will take care of sending the output to stdout via a pager. It is platform-agnostic, so it will work in Unix or Windows. click.echo_via_pager() will try to use decent defaults for the pager to be able to show color codes if necessary:

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.contrib.completers import WordCompleter
import click

SQLCompleter = WordCompleter(['select', 'from', 'insert', 'update', 'delete', 'drop'],
                             ignore_case=True)

while 1:
    user_input = prompt(u'SQL>',
                        history=FileHistory('history.txt'),
                        auto_suggest=AutoSuggestFromHistory(),
                        completer=SQLCompleter,
                        )
    click.echo_via_pager(user_input)

Editor

One of the niceties mentioned in my previous article was falling back to an editor when the command gets too complicated. Once again click has an easy API to launch an editor and return the text entered in the editor back to the application:

import click
message = click.edit()

Fuzzy Finder

Fuzzy Finder is a way for users to narrow down the suggestions with minimal typing. Once again, there is a library that implements Fuzzy Finder. Let's install the library:

pip install fuzzyfinder

The API for Fuzzy Finder is simple. You pass in the partial string and a list of possible choices, and Fuzzy Finder will return a new list that matches the partial string using the fuzzy algorithm ranked in order of relevance. For example:

>>> from fuzzyfinder import fuzzyfinder

>>> suggestions = fuzzyfinder('abc', ['abcd', 'defabca', 'aagbec', 'xyz', 'qux'])

>>> list(suggestions)
['abcd', 'defabca', 'aagbec']

Now that we have our fuzzyfinder, let's add it into our SQL REPL. The way we do this is to define a custom completer instead of the WordCompleter that comes with prompt-toolkit. For example:

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import Completer, Completion
import click
from fuzzyfinder import fuzzyfinder

SQLKeywords = ['select', 'from', 'insert', 'update', 'delete', 'drop']

class SQLCompleter(Completer):
    def get_completions(self, document, complete_event):
        word_before_cursor = document.get_word_before_cursor(WORD=True)
        matches = fuzzyfinder(word_before_cursor, SQLKeywords)
        for m in matches:
            yield Completion(m, start_position=-len(word_before_cursor))

while 1:
    user_input = prompt(u'SQL>',
                        history=FileHistory('history.txt'),
                        auto_suggest=AutoSuggestFromHistory(),
                        completer=SQLCompleter(),
                        )
    click.echo_via_pager(user_input)

Pygments

Now let's add syntax highlighting to the user input. We are building a SQL REPL, and having colorful SQL statements will be nice.

Pygments is a syntax highlighting library with built-in support for more than 300 languages. Adding syntax highlighting makes an application colorful, which helps users spot mistakes—such as typos, unmatched quotes, or brackets—in their SQL before executing it.

First install Pygments:

pip install pygments

Let's use Pygments to add color to our SQL REPL:

from prompt_toolkit import prompt
from prompt_toolkit.history import FileHistory
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.completion import Completer, Completion
import click
from fuzzyfinder import fuzzyfinder
from pygments.lexers.sql import SqlLexer

SQLKeywords = ['select', 'from', 'insert', 'update', 'delete', 'drop']

class SQLCompleter(Completer):
    def get_completions(self, document, complete_event):
        word_before_cursor = document.get_word_before_cursor(WORD=True)
        matches = fuzzyfinder(word_before_cursor, SQLKeywords)
        for m in matches:
            yield Completion(m, start_position=-len(word_before_cursor))

while 1:
    user_input = prompt(u'SQL>',
                        history=FileHistory('history.txt'),
                        auto_suggest=AutoSuggestFromHistory(),
                        completer=SQLCompleter(),
                        lexer=SqlLexer,
                        )
    click.echo_via_pager(user_input)

Prompt Toolkit works well with the Pygments library. We pick SqlLexer supplied by Pygments and pass it into the prompt API from prompt-toolkit. Now all user input is treated as SQL statements and colored appropriately.

Conclusion

That concludes our journey through the creation of a powerful REPL that has all the features of a common shell, such as history, key bindings, and user-friendly features such as auto-completion, fuzzy finding, pager support, editor support, and syntax highlighting. We achieved all of that in fewer than 20 statements of Python.

Wasn't that easy? Now you have no excuses not to write a stellar command-line app. These resources might help:

Learn more in Amjith Ramanujam's  PyCon US 2017 talk, Awesome Commandline Tools, May 20th in Portland, Oregon.

User profile image.
Amjith Ramanujam is a senior software engineer at Netflix. His team is responsible for keeping Netflix services running in the face of extreme adversity. In other words, his team is in charge of doing regional failover. In his spare time he writes modern CLI tools. He is the creator of pgcli and mycli. You should say hi to him on twitter.

3 Comments

I found configshell-fb very useful for a command line tool creation. targetcli is based on it.

Hi,
From the final code, I don't see the editor portion.

You're right. I didn't add that in because I'd have to show how to trigger the editor launch.

It is typically done in pgcli and mycli by appending a `\e` at the end of the query.

For example: `SELECT * FROM tabl1 \e` and pressing enter will launch the editor and fill the contents of the editor with the query. Then you can edit the query and quit the editor and the command will be filled back into the prompt.

It can be implemented by reading the query and checking if the query `endswith('\e')` and then take appropriate action.

I hope that help you get started.

In reply to by Andrew Goh L K (not verified)

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.
 

Get the highlights in your inbox every week.