Create Your First CLI Application With Python Click

A simple guide to building command-line interfaces in Python

mugoh mwaura
Better Programming
Published in
11 min readJun 2, 2019

Metasploit welcome shell

Life at the terminal is sweet. It’s fast powering up tasks, makes multitasking feel remarkably easy and allows you to keep resources that would otherwise be used by a fancy, but unnecessary, GUI.

Think about having to load up a graphical interface Version Control tool every time you want to track a change in your file — it’s scary how time-wasting that is. That’s our need for command-line interfaces on applications. When you get the itch to create a CLI for an application for the use of you or your team, you scratch it, of course, by building one!

In this article, well explore Python Click for creating command-line interfaces. We’ll familiarize ourselves with Click’s most useful features and see them at play. To do so, we have to get our hands dirty by creating a media file convertor CLI application — a little dirt never hurt!

If you would like to jump right into the complete project first, the code is here.

In the first part of our Python click tutorial, we cover a general but pretty cool Command Line Interface features undirectly related to click. If you’re about to create your first CLI app, you’ll find this bit useful. But for a direct dive, go straight to the section Bringing Click into the Picture.

So, Why a Command-Line Library?

Click is a Python library. It’s possible to create your own CLI from scratch. Take a look at this prompt:

$ >>> Make the world go round?
A little [default]
Yes
No

It’s a simple prompt that asks the user to select from three available choices. We can simply have this with the command below:

user_choice = input('Make the world go round?\n' +
'A little [default]\n' + 'Yes\n' + 'No\n')

Just like that, we can interact with the user at the command line. We’re taking a leap of faith and hoping that the user gives us a response from the available choices — which may not happen. This takes us to the issue of parsing user inputs.

We have to be ready for the user who replies “spin it” or something fancier like “I’m not sure”.

By looping with that condition, we restrict our user to giving us one of the three options. That should do it.

But…remember we wanted “A little” to be our default option if the user doesn’t specify anything. We tweak our code a bit to accommodate this. We should end up with something along these lines:

It might lessen the excitement, noticing how much patience it takes to parse a single user input. Most of the effort has gone into handling side cases of users who don’t like to follow instructions.

It would also be better if the app could specify the error in their input, rather than just repeat the same line — another consideration.

It’s possible to do this for a complete CLI app — parse each prompt, have descriptive error messages, give the user frequent feedback (we hate guessing — remember the last time you used dd). Importantly, we should be asked if we’re sure when we’re about to do something that seems silly.

We’re getting our hands dirty. We should be focusing on feature delivery. Command-line libraries are here to take away this pain so we can focus on the cool part. Remember, dirt doesn’t hurt — as long as it’s just a little dirt!

Photo by Zach Kadolph on Unsplash

Jumping Into Argument Vectors (Argv)

In the example spinning the world, we used input to interact with the user. However, it would make more sense to allow the user to supply their input directly when launching the application.

To run what we currently have, we would use:

>>> python spin_world_v2.pyMake the world go round?
A little [default]
Yes
No
: # User gives input

The user has to wait for the app to run to give their desired input. What we think would be a well-behaved script is:

>>> python spin_world_v2.py 'a little'# Then the world spins. Of course, a little

That way, we won’t have to wait for an input prompt to ask the user if they want the world to spin. We use argv to do this. Shall we tweak our first script a little more?

Running the script produces the following results:

Option 1 - Known argument
>>> python spin_world_v2.py 'a little'

# Output
[spin_world_v3.py 'a little'] # Our argv list Spinning this thing a little
Option 2 - Unknown argument
>>> python spin_world_v2.py 'some more'
# Output [spin_world_v3.py 'a little'] # Our argv list

Use with: a little yes, or no. Cool?

We’ve created a command-line script. All command-line apps use the argv concept when requiring the user to supply an argument while starting the application. Now, it will be sure to click when we have a look at the same concept, but using click library.

Interface Options

We use the shell ls all the time. We can learn some tips about interfaces by having a look at it.

Running ls in a terminal window:

ls
ls on home directory [Source]

It’s a simple tool. We can use it without calling an argument and be happy.

But would the use of an argument make us happier? Let’s see.

 ls -l
Running ls -l [Source]

Here, we made the tool a little noisy by requesting details. It throws at us the file permissions, the owner, the group, size, time, and name. It worked! In this case, -l is an option. Let’s see docs on its use:

>>> ls --help
Usage: ls [OPTION]... [FILE]...
# Run this to see the complete [OPTIONS] list

We see ls takes only two arguments — the option, and an optional path. We can vary the taste of information we get on our files and sub-directories by playing around with the listed options.

Interface Commands

ls has options. However, we can say it has no commands. The use of commands is to group related options of a CLI.

tool command --option

We can have hundreds of options in hundreds of commands. Why not place everything in a single list of options? That’s like filling your house with traps so trap admirers can come and visit you! Maintenance of your application will be difficult and users will find it hard to work with more complex features.

Let’s go back to our world spinning example and see something cool about commands.

The spinning of the world is in our control. Perhaps we can specify when the spinning occurs? Our script could have an interface command that allows us to set the time the spinning should take place.

python spin_world_v3.py time 0900 1700

Here, we set the time interval for which we want the world to spin. From 9 am (0900 hrs) because that’s when we take breakfast, to 5 pm (1700hrs) when we should be leaving work. Since these are working hours, we might want the world to spin faster than usual during this period.

So we give our application a spin command where we can specify the spinning speed.

python spin_world_v3.py time 0900 1700 --speed 1500m/h

This has given us a simple understanding of the CLI commands concept.

Bringing Click into the Picture

We’ve looked at how command-line tools work and the basics of creating a command-line interface.

Now, let’s create our media-convertor-CLI app. Our application should allow us to convert a media file to a format of our choice. We often use video to audio-conversion tools— it will be exciting to have one that we can tweak ourselves.

App setup

Our directory structure will look like this:

| audioConvertor  |-convertor
|- __init__.py
|- utils
|- __init__.py
|- cli.py
|- tests
|- __init__.py
- setup.py

We create the interface of our app in the file cli.py.

Adding usage options

To convert a file, we need to know its location. So we need to allow the user to tell our application where to get this file. We start building our app by creating a simple script that does that.

We give our users an option --input_directory to specify which file to convert. We can run the script as follows:

$ python cli.py --input_directory Videos/musicVideo.mp4# Output
Videos/musicVideo.mp4

Our script echoes back the file the user has specified. It works!

How will our users know how to interact with the app? A short help menu works a charm. With Click, we get this easy and for free. Let’s check it out by running:

$ python cli.py --help# OutputUsage: cli.py [OPTIONS]audioConvertor is a command-line tool that helps convert video files to audio file formats.example: python cli.py -i input/file/path -o output/pathOptions:
-i, --input_directory TEXT Location of the media file to be converted
--help Show this message and exit.

Note the convenience. -i is also interchangeable with --input_directory. The help example says an output path is necessary too.

example: python cli.py -i input/file/path -o output/path

We can get it working this way by adding the following lines to our script:

The added lines are marked ++. Adding an option is as easy as calling @click.option and passing our string to the decorated function. Remember to pass your options to main in order of creation.

Parsing user options

It would be good manners to confirm the existence of a file before we attempt to convert it. We can do so like this:

The script should now terminate if a user provides a non-existent media file. But with Click, parsing and validation are handled for us. The snippet of code below serves the same purpose as what we’ve just done above:

The magic happens at line six. By supplying the type parameter to Click’s options command, we can tell Click to ensure the user gives as --input_directory as a path, and that it exists.

Click gives us plenty of arguments to use in an option. We get to see more of them in play as we progress.

Adding commands

Using commands will allow us to isolate different features of our application for the convenience of users. It also makes it easier to add new options. For example, we have two options — it would make sense to nest them in a command that describes what they do.

python cli.py convert -i input/file/path -o output/path

We want to bundle our options in a convert command, so our app can be used as shown. What’s something else we would want our app to do besides converting? If we could play our converted songs, that would be nice. So, if we use it as shown below, something should pop out of our speakers:

python cli.py play --playlist path/to/audio

We know how we want our app to work. Let’s get to it.

Grouping the commands

The first thing we do is add the commands play and convert to our app.

Let’s go through this script.

We have three functions: — main, load_files, and load_audio. The function main is helping us group our two commands with Click by decorating it with @click.group. This is our first step.

After that, notice how easy it is to add a command — we call @main.command then specify the options we think will make us happy.

There’s this line hovering around:

@click.pass_context

Whenever we want to use an argument specified in command but in a different function, we pass its context. This is done by storing it in the click context object dictionary. See what we do in line 21:

ctx.obj[‘VERBOSE’] = verbose

This allows us to access the value of VERBOSE in any other function with a click decorator by passing in ctx. (Remember to call main with the obj argument as in line 52). For example:

def load_files(ctx, input_directory, output):
"""
: Convert video file input to audio.
"""
if ctx.obj.get('VERBOSE'):
# shout a lot
else:
# convert quietly

We introduced some new arguments to our command options too. Here’s what they help us achieve:

  • required: mandatory input.
  • multiple: allow users to add many parameters e.g convert -i video_1 -i video_2.
  • nargs: almost same as multiple e.g convert -i video_1 video_2 video_3

In our case, nargs would help us specify multiple input files.

  • is_flag: A boolean option that doesn’t need a parameter`.

Convert — option

Let’s list out all our decorators for function (load_files) running the convert command.

That’s a list of options for one command on our CLI. Now let’s dive into processing the parameters received from the user. For easier maintenance, we will isolate all our processing functionality to a new file, in a class called Convertor.

| audioConvertor|-convertor
|- __init__.py
|- utils
|- __init__.py
|- cli.py
|- formats.py
|- tests
|- __init__.py
- setup.py

Since our focus is on the CLI, we won’t dwell on the Convertor class. But I made an effort to document it quite nicely, so don’t worry about it.

Let’s add these lines to the top of our cli.py:

import click
from formats import Convertor # +
convertor_instance = Convertor() # +
processing user input

Let’s toy around a bit on our interface with the above options:

$ python3 convertor/cli.py convert -i /root/Videos/# Output
/root/Videos/ is a directory. --recursive Needed for a directory

Let’s point our app to a file and attempt to convert it again:

python3 convertor/cli.py convert -i Videos/snoring_noises.avi
Input specified as file name
.
.
Conversion Complete
saved: snoring_noises.mp3

The app should prompt us to install ffmpeg, which is the media convertor library we shall use. This is getting exciting!

Next, let’s allow the user to convert multiple video files at once by running:

python3 convertor/cli.py convert -i /root/Videos/* -o /root/converted_music

This command should convert for us all video files in the Videos folder to audio and save the output to the directory converted_music. Adding the following code to the load_files function should do it:

++ in the `load_files` function

Play — option

The play command decorates the load_audio function:

The app should now allow us to load a playlist of our choice.

Chaining Commands

Whenever we want to convert a file then play its audio, our CLI application restricts us from running the convert and play command individually. That’s good enough, but wouldn’t it be better if we could use the interface as follows?

python convertor/cli.py convert -i my_video_file.mp4 play

To use more than one command at a go, we pass the chain argument to our multi-command:

@click.group(invoke_without_command=True, chain=True)

That’s just the first step. We are invoking convert first. When we run play later, the location of our converted file shall need to be known. For every successful conversion, our interface should know the path of the saved file(s).

ctx.obj[‘PLAYLIST’] = convertor_instance.get_file_save_path()

We add the PLAYLIST key as a context accessible to all our sub-commands. get_file_save_path is the function that does this. The above line shall be added in two places in the load_files function:

Some Additional Considerations

Individual and multiple file conversion

To differentiate between the single and multiple file inputs, one tweak would be allowing the user to set a recursive option. The recursive flag, during processing, will let us know if we are to convert a single or more than one file.

Additionally, to check if the input is a directory, we recurse through all its children and find any valid file matching a video format.

if os.path.isfile(input_directory) and not recursive:
# convert single file
else:
# convert multiple files

That solves it for us. We can even match input files recursively using the wild-card, *:

python convertor/cli.py convert -i Videos/snoring*.mp4 play

Optional arguments

If we attempted to run the above command, we might get an output such as this:

python convertor/cli.py convert -i Videos/snoring*.mp4 playUsage: cli.py play [OPTIONS]
Try "cli.py play --help" for help.
Error: Missing option "--playlist" / "-p".

It means playlist is a required argument. To make it optional, required set to False as follows:

@click.option('--playlist', '-p', required=False, type=click.Path(exists=True),
help="Folder containing audio files to be played")

Conclusion

That’s it, folks.

We’re now familiar with command-line interfaces and have learned how to interface an application to the terminal, using Click. I hope this Python Click tutorial has helped you gain something exciting and useful. Remember you can have a tour of the complete project on its GitHub repo.

You are awesome. Make your next CLI better.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

mugoh mwaura
mugoh mwaura

Written by mugoh mwaura

Fascinated by bread | Learning to smile | RL & Decision control

Responses (1)

Write a response