Click: a beautiful python library to write CLI applications

An image of a terminal running top command
An image of my terminal running top command
  • cleo: a CLI toolkit used by poetry, a python dependency manager (oh, I have a nice introduction to poetry here).
  • cliff: The CLI toolkit created by the Openstack team and from which all their command line utilities are created. It has a nice oriented object approach for writing CLI.
  • docopt: An old competitor to click which parses docstring to dynamically create CLI.
  • fire: An unofficial google project using objects to dynamically create CLI.

Basic concepts

Parameters

Arguments

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

Options

  • Automatic prompting for missing input
  • Act as flags
  • Pulling values from environment
  • Documented via the click parameter constructor.

Installation

pip install click

Project setup

[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"
[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"
...

Usage

Our first CLI: hello

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}!')
[tool.poetry.scripts]
hello = "scripts.hello:cli"
$ hello --help
Usage: hello [OPTIONS]
Greets a user who gives his name as inputOptions:
-n, --name TEXT Name to greet
  • You have an automatic “ — help” option which prints the docstring of the function as the CLI documentation.
  • Options are documented automatically when printing CLI help. The “help” argument passed in the option constructor is used like you can see. Also when defining the option, we didn’t specify a type, but you can see TEXT printed. So, note that if you don’t specify the type of an option or argument, it will be a string (aka TEXT) by default.
$ hello -n kevin
Hello kevin!
$ hello --name=kevin
Hello kevin!
$ hello
Your name: kevin
Hello kevin!
  • You can pass option using the short syntax “-n” or the long syntax “ — name” as we defined it in the parameter constructor.
  • If you don’t pass the name option, you are automatically prompted to do it. Remember the prompt argument used in the option constructor with the value “Your name”. This is one of the features of the options.
  • For details about option naming, I recommend you to read this short paragraph of click documentation.

Second example: clone of cat command

$ pycat
Usage: pycat [OPTIONS] FILE
Try 'pycat --help' for help.
Error: Missing argument 'FILE'.$ pycat hello.txt
Hello world!
  • We have an example usage of a click argument. The type used is a click.File which opens the given filename in read mode by default. The file is automatically closed when the function finishes its execution. This is why you see I don’t use the close method in the definition of the function.
  • If you don’t specify a filename when calling the CLI you have an error message, the same if you pass a file that doesn’t exist. Click handles these common mistakes automatically. And if you pass a correct file (replace “hello.txt” in my example by a file on your computer), you will have its content displayed.
  • Now we add an option “-n” which acts as a boolean flag, you can guess it with the argument is_flag. This means that if you don’t pass this option when using pycat, the behaviour will be the same as before. If you use it like this “pycat -n <file>” then you will have the number of line printed just before the line itself.
  • As an exercise, you can try to implement the remaining options of the cat command.

Test click CLI

Snippet code of pycat command
  • Line 1, we import the CliRunner useful for testing CLI applications
  • Line 6, we use the default pytest fixture tmp_path which is a Path object representing a temporary directory. If you don’t know about pytest or fixtures, I recommend to read its documentation or at least this section.
  • Line 10, we invoke our CLI by passing arguments as a list as a second parameter. If you wonder about the resolve method, it is just to have the full path of the temporary file created.
  • Line 12, as you can see, the default exit code when all goes well is 0. It will be different if there is an error.
  • Line 13, note the “\n” at the end of the text. This is because click.echo by default adds a new line when printing text, just like the builtin print function does 😉
  • As an exercise, you can also test when adding the flag “-n” to the command. Tip: you can use the parametrize feature to don’t have to create a second test, you will just need to adapt what is passed to the invoke method and the output tested at the end.
  • For more information about testing, you can read this section of click documentation.

Third example: less command

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
  • If you want to test the CLI and see the effect, you will need to read a long file or drastically reduce the size of your terminal window. I use the lorem ipsum generator to help me .😄
  • All the logic of the pager is done by click.echo_via_pager, you can read its api documentation if you are curious.

Fourth example: 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
  • I use the click.Path type for the argument. I could always have used the click.File but I wanted to show you an example usage of click.Path and open_file helper function. The “open_file” helper is a tiny wrapper around the open builtin function with additional features to handle stdin and stdout, atomic writes, etc.. The api documentation can be found here.
  • I’ll let you understand how the script works, it shouldn’t be too difficult. I just put here the links to functions you might not know, I’m thinking of any and re.split.

Fifth example: A reverse pointer address helper

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
  • Lines 5 to 12, we define the custom parameter type. We just need to inherit from click.ParamType and redefine the convert method. It takes the value, the corresponding parameter object and a context which holds the state of the CLI. The last two arguments are generally only useful for error messages.
  • Line 10, we try to convert the given value to an ip address object and if it things go wrong, we return an error message line 12 passing the parameter and context.
  • As a little advertising, I have a project implementing a lot of custom parameter types. The documentation is available here.

Sixth example: A simple implementation of an HTTP client

$ poetry add httpx pygments
$ 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
  • As you can see in the first use case, we must be able to pass an option multiple times, in this case “-q” which represents a query parameter. For that the option constructor has a boolean attribute multiple to allow it.
  • The value of the option is in the form “key:value” so we need to parse it to get the values separately and print errors if there are any.
  • In the last use case, we use a flag option “ — no-verify”. Note that this attribute is placed directly after httpx command, not after the get subcommand. But we will need this information in the subcommand, so how do we do? The answer is the click context object. It is an implicit object created by click when invoking a CLI function which holds some state about the application executed. It has a special obj attribute where we can store arbitrary data and access it in subcommands via the use of the pass_obj decorator. You will see an example below.
  • httpbin.org is a website created by requests author to help developers to test efficiently their http client.
  • Fun fact: the creator of httpx also created a CLI called httpx. 😆
snippet code of httpx command
  • If you test this code, you will have the expected behaviour for the get subcommand. I also implemented the post subcommand. Again this is not a complete implementation, for example the json data only handles strings, not other types support by the json standard library.
  • The file http_util.py contains a function to pretty print HTTP data in the terminal. If you don’t understand it, I recommend to go through the pygments documentation.
  • In the main file, lines 14 to 18, we define a function to parse an option value and raises click exception if it is not correct.
  • Lines 21 to 44, we define functions to get all http attributes we care about like headers, cookies, query parameters, etc.. I hope you are familiar with type annotations, if you are not, you should. It improves code readibility and if you have the right tools like mypy or IDE like Pycharm that understand annotations, it can prevent you from some common typing bugs.
  • To declare a CLI that will contain subcommand, we decorate the main callback with click.group line 47.
  • Line 49, we pass the click context for this CLI and at line 52 we instantiate the RequestData class holding some state we need in the subcommands. The resulting object is passed to the obj attribute of the context.
  • Line 55, note that the subcommand is defined with cli.command and not click.command. Yeah we use the main callback which is transformed by click.group decorator.
  • Line 60, I use the click.pass_obj decorator to pass the object created at line 52.
  • For the rest, I leave it to your understanding this shouldn’t be too difficult.
  • As an exercise, you can try to implement other subcommands like patch, put, etc... You can also add authentication (basic, digest). Complete the handling of json data, etc… Don’t forget that the best way to learn is to practice. 😉

Bonus: Image downloader

$ poetry add anyio rich
$ 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! 🌟
  • To test it, I take images from this website. It is the lorem ipsum for images.😄
  • About the code, lines 11 to 19, we define a function to get the list of urls given a file as input.
  • Lines 22 to 34, we define an async function to download the image and place it in the destination folder specified by the user.
  • Lines 37 to 46, we define the main async function run by anyio at line 72. This function runs concurrently all the downloads.
  • Line 49, we use a cool option defined by click to be able to request the CLI version. You can look my CLI checks above to see how it works.
  • Line 50, we define the context_settings argument to be able to use “-h” in addition to “ — help” when requesting the help message of the CLI. More information can be found here.
  • The rest should not be difficult to understand.
  • Take care when defining arguments of their position in the code. If you see again the help message, you will notice the order in which click CLI expects the arguments, file before destination. If you look at the code, you see that the definition of the file argument is on top of the definition of the destination argument. This can save you from some debugging time, trust me.

--

--

--

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.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Build a real-life serverless app with AWS Amplify

System Design: Everything You Need to Know About

API Integration | SMS API | Bulk SMS API

Testing file uploads to AWS S3 with IAM user credentials in Postman

3 Open-Source Projects You Can Join Right Now!

An Honest Look at Dagger

5 phases to create a 5-year growth plan as a developer

An Overview of Projects from Legend X Hackathon — V

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
Kevin Tewouda

Kevin Tewouda

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.

More from Medium

Easy Python setup on Apple Silicon with Homebrew

Python | A very simple progress bar — tqdm

Two Steps To Turn A Python Script Into A macOS Application Installer

Extracting Computer and Required Steam Specs Using Python