Introduction

Command line interfaces (CLIs) have been with us since the digital stone ages. It’s how a lot of people got started with computers and still one of the ways professionals use to interact with computers today. That is particularly true for most developers and sysadmins that work in a server environment. CLIs might not be as flashy and colorful as GUIs, but they are really useful. Essentially anything you can do with a CLI you can automate easily and that’s one of the main reasons why I love CLIs. In this article I’m going to show you how to build a such a CLI.

I <3 CLI

To do that we’ll use Python and the Click library. This article will be a first look at Click and how you can use it. I’ll explore the library and its use cases further in future posts.

You can find all of the code in this article in this repository on Github.

Installation

I usually try to keep my external dependencies as small as possible, because Python tries to come “batteries included” and for the most part this works. For building CLIs you could also rely on the argparse module from the Python standard library - I chose not to do that, because it felt quite cumbersome compared to Click. Click is not part of the standard library and as such an external depedency, which you install via the Python package manager pip.

pip install click

At the time of writing this will install Click in version 7.1.x for you. The cool thing is, that Click itself doesn’t add any further external dependencies which means it’s still pretty lightweight.

Now that we’ve set up Click for our project, let’s continue with a basic hello world example.

Hello world

It’s tradition for any programming tutorial to start with a hello world application. In our case this will be a CLI app without any function besides its documentation.

import click

@click.command()
def cli():
    """
    Prints "Hello World!" and exits.
    """
    click.echo("Hello World!")

if __name__ == "__main__":
    cli()

If we run this code, the result looks like this:

$ python hello_world_cli.py
Hello World!

A little underwhelming, I know. But it can do more. We just need to add the --help argument for Click to create a nice help page based on the docstring of our cli function:

$ python hello_world_cli.py --help
Usage: hello_world_cli.py [OPTIONS]

  Prints "Hello World!" and exits.

Options:
  --help  Show this message and exit.

That’s convenient and all we had to do is add the @click.command decorator to our function for Click to know this is something it needs to do its magic on. If this is the first time you’ve come across decorators, they are an advanced programming pattern that make it possible to add more functionality to a function without modifying the function code - for an introduction I recommend this article.

You might have noticed that I used click.echo() to print to the terminal. In this case the built in print() function would have done the same, but echo let’s us do a couple more things (and click.secho even more, but more on that in a future post).

Now you might say: ‘Come on, Maurice, this is something I could have achieved in a couple lines of code with argparse without requiring an external library…' and you’d be right. Hello World programs aren’t designed to be useful, they exist to give you a first glimpse of the structure that awaits you. With Click you can expect most of these structures to be based on decorators that are added to regular functions. If I succeeded, you’re not repelled by the mere thought of working with Click, so let’s continue with a slightly more interesting example.

Slightly more interesting example

Let’s have a look at a slightly more advanced example that will allow me to point out some of the features Click offers.

import operator
import click

@click.command()
@click.argument("a", type=click.INT)
@click.argument("b", type=click.INT)
@click.option(
    "--operation", "--op",
    type=click.Choice(["add", "subtract", "multiply", "divide"]),
    default="add",
    help="Operation to perform on the operands, default: add")
def cli(a: int, b: int, operation: str):
    """
    This is an implementation of a basic calculator.

    By default this adds the two operands A and B.
    You can change that using the --operation parameter.
    """

    operation_sign_mapping = {"add": "+", "subtract": "-",
                              "multiply": "*", "divide": "/"}

    operation_callable_mapping = {
        "add": operator.add, "subtract": operator.sub,
        "multiply": operator.mul, "divide": operator.truediv,
    }

    result_of_operation = operation_callable_mapping[operation](a,b)
    sign = operation_sign_mapping[operation]

    click.echo(f"{a} {sign} {b} = {result_of_operation}")

if __name__ == "__main__":
    cli()

You’ll probably recognize the outer shell from the hello world example. Ignore the content of the cli() function for now. Let’s focus on the new decorators that have been applied to the cli() function.

From working with Python you’re probably familiar with position based arguments and keyword arguments. CLIs allow for a similar concept that are exposed through the argument-decorator and the option-decorator respectively. To get an idea about the differences, let’s look at the help page, that Click generates for us:

$ python calculator_cli_v1.py --help
Usage: calculator_cli_v1.py [OPTIONS] A B

  This is an implementation of a basic calculator.

  By default this adds the two operands A and B. You can change that
  using the --operation parameter.

Options:
  --operation, --op [add|subtract|multiply|divide]
                                  Operation to perform on the operands,
                                  default: add

  --help                          Show this message and exit.

As you can see from the usage section, the program expects two operands A and B to be passed in that will be added by default. We can change this behaviour (i.e. adding) by using either the optional --op or --operation parameters with the list of possible values that are shown here.

If we have a closer look at the function decorators you can see, that I’ve specified a type argument on all of them. Click supports type checking with a range of predefined types and the ability to create custom types - this essentially takes care of input validation for us. To see it in action, let’s input some invalid values into our program.

$ python calculator_cli_v1.py a
Usage: calculator_cli_v1.py [OPTIONS] A B
Try 'calculator_cli_v1.py --help' for help.

Error: Invalid value for 'A': a is not a valid integer

$ python calculator_cli_v1.py 1 2 --op does_not_exist
Usage: calculator_cli_v1.py [OPTIONS] A B
Try 'calculator_cli_v1.py --help' for help.

Error: Invalid value for '--operation' / '--op': invalid choice:
    does_not_exist. (choose from add, subtract, multiply, divide)

As you can see it even gives us hints on the range of valid values, which is convenient.

On the @click.option decorator I used the help attribute to give a helpful hint when looking at the output of --help. You might wonder why this isn’t present on the others - that’s because the parameter is only supported for option decorators. Click wants you to explain what the arguments do in the docstring of the function in order to align more with the conventions other CLIs use. Setting the default allows us to always pass in a value to our function even if the parameter isn’t set explicitly. We could also make the option required, by passing the required=True argument to the option decorator.

@click.command()
@click.argument("a", type=click.INT)
@click.argument("b", type=click.INT)
@click.option(
    "--operation", "--op",
    type=click.Choice(["add", "subtract", "multiply", "divide"]),
    default="add",
    help="Operation to perform on the operands, default: add")
def cli(a: int, b: int, operation: str):
    """..."""

Looking at the function definition, we can see that the arguments generated by the decorators are passed in to the function in the order that the decorators are applied in. The int and str in the function definition are just standard Python type hints, they don’t affect what Click does.

The business logic is relatively simple:

def cli(a: int, b: int, operation: str):
    # ...

    operation_sign_mapping = {"add": "+", "subtract": "-",
                              "multiply": "*", "divide": "/"}

    operation_callable_mapping = {
        "add": operator.add, "subtract": operator.sub,
        "multiply": operator.mul, "divide": operator.truediv,
    }

    result_of_operation = operation_callable_mapping[operation](a,b)
    sign = operation_sign_mapping[operation]

    click.echo(f"{a} {sign} {b} = {result_of_operation}")

The operator module from the standard library is used to call the builtin functions that implement +, -, *, and /. The correct operator is taken from the operation_callable_dictionary based on the value that’s passed in through operation. We don’t need to do any additional input validation as that’s done through Click as described above. Creating the output is achieved with the relatively new f-Strings that are used to interpolate the values. The result is then printed using the click.echo() function I mentioned earlier.

Calculator v1 in action

Let’s now take a look at the calculator we just built in action. It’s about as exciting as you’d expect, but you can plainly see how the inputs are handled and passed through to the business logic.

Demo Gif

Conclusion

In this article we’ve take a first look at the Python library Click and how it can help you build cool CLIs for your projects. Future posts will cover more advanced patterns and features of the library. Thank your for reading this till the end! I’d love to get your feedback/questions/comments via Twitter (@maurice_brg).

References

Update 14.08.2020: Thanks to the kind feedback from the readers at reddit I was able to improve parts of the article.