Click: a beautiful python library to write CLI applications

Write simple and complex CLI applications in python using click.

An image of a terminal running top command
An image of a terminal running top command
An image of my terminal running top command

As developers, we often used command line interface (CLI) applications for different purposes like writing files with vim or emacs, running containers with docker, researching a pattern in any file using grep, etc… When I began programming, I have always wondered how do we write one? Then after some researchs, I found click. It was created by the same author of Flask, Armin Ronacher, who was frustrated to not find a suitable library to write Flask CLI application.

Before diving into the usage of click, I want to present you some alternatives that may be more suitable for you:

Personally, I find that click is more flexible than competitors, doesn’t try to guess and create dynamic stuff (remember the zen of python: “explicit is better than implicit”) and also has a nice community with plugins we can add on top of it to do nice things like auto-completion, command guessing, etc…

Basic concepts

Parameters

Click supports two types of parameters: options and arguments. Parameters in click have different types to handle numbers, file, datetime, etc.. The complete list can be find on this page. You can also define custom parameter types, I will show an example of how to do it afterwards.

Arguments

Arguments are less powerful than options. There are often used to specify file paths and urls. They cannot be documented like options in the click parameter constructor. The only way to document it is in the function docstring. Example usage:

import click


@click.argument('input_file', type=click.Path(dir_okay=False))
def cli(input_file):
"""INPUT_FILE represents the path to a file on your file tree"""
pass

Don’t try to run this example, it is just to show you how it looks like. 😁

Options

Options on the other side are full of features including:

We will see some of these features in the rest of the tutorial.

Installation

To install click, you can use pip.

pip install click

But I recommend you to use poetry which is far better than pip to manage dependencies and deploy applications at the moment I’m writing this article. I have a nice introduction on poetry here if you don’t know it. It is important to understand it to follow the rest of my tutorial because I will use it afterwards.

Note that at the time I’m writing this article the latest version of click is version 7.X (but there is a version 8 in preparation) and I will assume you use python3.7 or higher.

Project setup

To learn how to use click, we will create a project click_tutorial. So create a folder of the same name somewhere on your home folder. Inside it, run “poetry init”, this will help you to setup the pyproject.toml file interactively. When you get to the main dependencies definition, just install click. And for the dev dependencies section, install pytest.

If you don’t understand everything, I said before, just be sure to have a pyproject.toml file that looks like this:

[tool.poetry]
name = "click_tutorial"
version = "0.1.0"
description = "A tutorial about using click"
authors = ["le_woudar <XXX@XX>"]
license = "MIT"

[tool.poetry.dependencies]
python = "^3.7"
click = "^7.1.2"

[tool.poetry.dev-dependencies]
pytest = "^6.1.2"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

Ok, now create a scripts package inside click_tutorial. This is where we will write our cli applications. After that slightly modify the pyproject.toml file by adding the packages section under the license information. It should now look like this:

[tool.poetry]
name = "click_tutorial"
version = "0.1.0"
description = "A tutorial about using click"
authors = ["le_woudar <lewoudar@gmail.com>"]
license = "MIT"

packages = [
{ include = "scripts" }
]

[tool.poetry.dependencies]
python = "^3.7"
...

I don’t wrote the rest of the file, the idea here is to show you where to place the packages section. Now you can run “poetry install” and the project will be setup correctly.

Usage

Our first CLI: hello

Yeah let’s start with something traditional in the programming world, I named “hello world”. In our case we will greet the user entering his name. Copy the following content in a file hello.py inside the scripts package.

import click


@click.command()
@click.option('-n', '--name', prompt='Your name', help='Name to greet')
def cli(name):
"""Greets a user who gives his name as input"""
click.echo(f'Hello {name}!')

So here we defined a CLI with the decorator click.command(). We also define one option with the decorator click.option(). We pass the value of the option in the command function and use it to print the name of the user. Now add a section in pyproject.toml like the following:

[tool.poetry.scripts]
hello = "scripts.hello:cli"

On the left side, we give the name we want to our CLI. On the right side, we give the path to callable running this CLI. The pattern is “path.to.module:callable”. You will need to reinstall the project with “poetry install” to be able to run the CLI. Don’t forget to do it after adding a new script.

Before trying to run the example, open a shell at the project location and run “poetry shell”, this will activate the project’s virtualenv. It is to avoid prefixing all our CLI applications with “poetry run”. Now let’s check the help message.

$ hello --help
Usage: hello [OPTIONS]
Greets a user who gives his name as inputOptions:
-n, --name TEXT Name to greet

Key points:

Now let’s show some uses of our first CLI.

$ hello -n kevin
Hello kevin!
$ hello --name=kevin
Hello kevin!
$ hello
Your name: kevin
Hello kevin!

Notes:

Second example: clone of cat command

For our second example. We will create a simple version of the well known unix cat command. Create a new file “pycat.py” in the scripts package with the following content (tip: click on “view raw” at the end of the github code snippet if you want to copy paste the content).

Again don’t forget to add a script section under “tools.poetry.scripts” like we did before and reinstall the project to take in account our new CLI.

$ pycat
Usage: pycat [OPTIONS] FILE
Try 'pycat --help' for help.
Error: Missing argument 'FILE'.$ pycat hello.txt
Hello world!

Notes:

Now let’s add an option flag to print the line number next to the line itself.

Notes:

Test click CLI

Like any software we are writing, we need to write tests when developing our CLI. For this click provides a test runner to check output, return code or exception. Create a tests package near scripts and inside it create a file test_pycat.py with the following content:

Snippet code of pycat command

Notes:

Third example: less command

At this point you should know how to install a script in your project, so I won’t repeat it anymore. I’ll just display the code and make a few comments. This time, we will implement the less command. This command is useful when you have a long text to print. Do not name the CLI less to avoid clashing with the existant command, call it for example pyless.

Snippet code of pyless command
$ pyless lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In est velit, lobortis ut magna vel, venenatis ultrices magna. Maecenas accumsan mi ut metus vehicula, sed facilisis enim sollicitudin. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse quis tellus scelerisque, semper risus sed, efficitur erat. Proin semper quis leo iaculis sodales. Mauris in diam tortor. Aenean condimentum felis odio, non consequat nisi lacinia eu.
:
... # use the down arrow or Enter key to continue reading the text

Notes:

Fourth example: wc command

This time, we will implement the wc command.

Snippet code of pywc command
$ pywc scripts/pycat.py 
17 55 512 scripts/pycat.py
$ pywc -wl scripts/pycat.py
17 55 scripts/pycat.py

Notes:

Fifth example: A reverse pointer address helper

If you recall at the beginning of the article, I said I would show you how to implement a custom parameter type, well the time has come! And you’ll see that it’s pretty easy.

For this example, we will implement a CLI taking an IP address as input and returning the corresponding PTR name useful to get the PTR value. This is useful for network administrators dealing daily with DNS. It is not important if you know anything about it, just follow the logic of implementation of a custom parameter type. Fortunately all we need can be found in the standard library module ipaddress.

Snippet code of ptr command
$ ptr 127.0.0.1
1.0.0.127.in-addr.arpa
$ ptr 2001:db8::1
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa
$ ptr 4
Usage: ptr [OPTIONS] IP_ADDRESS
Try 'ptr --help' for help.
Error: Invalid value for 'IP_ADDRESS': 4 is not a valid ip address

Notes:

Sixth example: A simple implementation of an HTTP client

In this example, we will see how to implement a simple HTTP client like curl or more specifically in our case like httpie. The latter is a well-known CLI in python, if you don’t know it and deals with HTTP in your terminal daily, I recommend you to check it, it is really well done.

This will be the occasion to show you how to nest subcommands in a global command. Now install the following packages:

$ poetry add httpx pygments

Httpx is a modern http client that can support HTTP/2 and inspired by the revered requests library. Pygments is a syntax highlighter that will be used for a nice output display in the terminal. Let us see what usage we want to have before digging in the code.

$ httpx get http://httpbin.org/get -q foo:bar -q hello:world
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 325
Content-Type: application/json
Date: Tue, 01 Dec 2020 19:18:52 GMT
Server: gunicorn/19.9.0
{
"args": {
"foo": "bar",
"hello": "world"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "HTTPie/2.0.0",
"X-Amzn-Trace-Id": "Root=1-5fc6979b-38e769ff191d5526046ef5f4"
},
"origin": "91.170.251.103",
"url": "http://httpbin.org/get?foo=bar&hello=world"
}
$ httpx --no-verify get https://httpbin.org/get -q foo:bar -q hello:world
# again we need to have the same answer as before, the difference is that we use https instead of http and we don't want to check the certificate, hence the "--no-verify" option

Some comments:

Now the code:

snippet code of httpx command

Notes:

Bonus: Image downloader

As a final example, we will implement a CLI taken a bunch of images written in a file as input and downloading them in a directory of our choice. I will name it imgdl but you can give the name you want. To run this last example, you will need to install two new libraries.

$ poetry add anyio rich

Anyio is a library which will help us to download images concurrently. If you don’t know it, I have a nice tutorial here.

Rich is a library to enhance user experience in the terminal. You have many appealing features like text colorization, table and panel rendering, etc.. In our case it is the progress bar feature that will interest us. I will not explain how it works, you can check the relevant documentation for that.

Here is the code:

$ imgdl --version
imgdl version 0.1.0
$ imgdl -h # note that this time I use "-h" and not "--help"
Usage: imgdl [OPTIONS] FILE DESTINATION
Downloads multiple images given their urls in FILE and store them in
DESTINATION.
...
$ imgdl images.txt ./images
Downloading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00
Downloads are finished! 🌟

Notes:

So this is the end of this tutorial, hope you enjoy it as I enjoy writing it. If you have remarks/suggestions to make, do not hesitate to comment this article.

Take care of yourself and see you soon for a new tutorial. 😁

Déserteur camerounais résidant désormais en France. Passionné de programmation, sport, de cinéma et mangas. J’écris en français et en anglais dû à mes origines.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store