Building Delightful Command-Line Interfaces with Click

In software engineering, we talk a lot about creating intuitive and delightful user interfaces, which is to say, graphical user interfaces. But what about the command-line? At Ginkgo, our users are scientists and biological engineers, so some of the software that we write are best presented as command-line tools rather than web apps. Click is a powerful and easy-to-use Python library that you can use to create rich command-line interfaces, and I’ll be going over some of its basic features.

The Basics

Getting Started

One of the nice things about Click is that it’s easy to get started, and you can realize a lot of power without much boiler plate at all.

#!/usr/bin/env python3

# hello_world.py
import click

@click.command()
def hello_world():
  click.echo('Hello, world!')

if __name__ == '__main__':
    hello_world()

The @click.command() decorator is not terrifically useful on its own — it’s a starting point from which we will add more features to our command-line UI. click.echo is very much like print, but the result is more consistent across different terminal environments.

Arguments

We enrich the behavior of our command-line user interface by adding decorators to our main function. For example, we can use the @click.argument() decorator to specify that our “Hello, World!” tool takes an argument name, which will form a part of the greeting:

#!/usr/bin/env python3
import click

@click.argument('name')
@click.command()
def hello_world(name):
  click.echo(f'Hello, {name}!')

if __name__ == '__main__':
    hello_world()

Now, we can run the tool with the argument:

> ./hello_world.py Ray
Hello, Ray!

The decorator @click.argument('name') specifies that the command-line interface takes a single argument, which is passed to the main function as the named argument name.

Options

Specify command-line options also with a decorator:

#!/usr/bin/env python3
import click

@click.option('--punctuation', default='!')
@click.option('--greeting', default='Hello')
@click.argument('name')
@click.command()
def hello_world(name, greeting, punctuation):
  click.echo(f'{greeting}, {name}{punctuation}')

if __name__ == '__main__':
    hello_world()

Here, we have added two command-line options: --greeting and --punctuation. These options are passed into the command function as keyword arguments greeting and punctuation respectively (where Click generated the keyword argument names by parsing names of the options). We have set default values for both options, in case either option is left out when the command is invoked:

> ./hello_world.py Ray --greeting Bonjour
Bonjour, Ray!

We can also set an option as required:

#!/usr/bin/env python3
import click

@click.option('--punctuation', default='!')
@click.option('--greeting', required=True)
@click.argument('name')
@click.command()
def hello_world(name, greeting, punctuation):
  click.echo(f'{greeting}, {name}{punctuation}')

if __name__ == '__main__':
    hello_world()

So, this time, if we were to leave out the --greeting option:

> ./hello_world.py Ray
Usage: hello_world.py [OPTIONS] NAME
Try 'hello_world.py --help' for help.

Error: Missing option '--greeting'.

As you can see, the command function will not run, and the script quits with an error message and suggestion to invoke the --help option, which brings us to the next feature of Click that we will discuss.

Help Documentation

Click makes it easy to create rich and informative help documentation. Click automatically adds a --help option to all commands (which can be disabled by passing add_help_option=False to @click.command()).

Docstring Integration

Click integrates with Python docstrings to generate descriptions of commands for the help screen:

#!/usr/bin/env python3
import click

@click.option('--punctuation', default='!')
@click.option('--greeting', default='Hello')
@click.argument('name')
@click.command()
def hello_world(name, greeting, punctuation):
    """
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{punctuation}')

if __name__ == '__main__':
    hello_world()
> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] NAME

  Prints a polite, customized  greeting to the console.

Options:
  --greeting TEXT
  --punctuation TEXT
  --help              Show this message and exit.

Here, by adding a docstring to the command function, we are simultaneously helping developers by documenting our source code, as well as our end-users by providing a useful help screen message.

Documenting Options and Arguments

Document your options by using the help argument:

#!/usr/bin/env python3
import click

@click.option('--punctuation', default='!', help="Punctuation to use to end our greeting.")
@click.option('--greeting', default='Hello', help="Word or phrase to use to greet our friend.")
@click.argument('name')
@click.command()
def hello_world(name, greeting, punctuation):
    """
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{punctuation}')

if __name__ == '__main__':
    hello_world()
> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] NAME

  Prints a polite, customized  greeting to the console.

Options:
  --greeting TEXT     Word or phrase to use to greet our friend.
  --punctuation TEXT  Punctuation to use to end our greeting.
  --help              Show this message and exit.

Note that you cannot specify help text for arguments — only options. You can, however, provide a more descriptive help screen by tweaking the metavars:

#!/usr/bin/env python3
import click


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our greeting.")
@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.argument('name', metavar='NAME_OF_OUR_FRIEND')
@click.command()
def hello_world(name, greeting, punctuation):
    """
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{punctuation}')


if __name__ == '__main__':
    hello_world()
> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] NAME_OF_OUR_FRIEND

  Prints a polite, customized  greeting to the console.

Options:
  --greeting TEXT                 Word or phrase to use to greet our friend.
  --punctuation PUNCTUATION_MARK  Punctuation to use to end our greeting.
  --help                          Show this message and exit.

Types

Click gives us some validation right out of the box with types. For example, you can specify that an argument or option must be an integer:

#!/usr/bin/env python3
import click


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our greeting.")
@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.option('--number',
              default=1,
              type=click.INT,
              help="The number of times to greet our friend.")
@click.argument('name', metavar='NAME_OF_OUR_FRIEND')
@click.command()
def hello_world(name, greeting, punctuation, number):
    """
    Prints a polite, customized  greeting to the console.
    """
    for _ in range(0, number):
        click.echo(f'{greeting}, {name}{punctuation}')


if __name__ == '__main__'
    hello_world()
> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] NAME_OF_OUR_FRIEND

  Prints a polite, customized  greeting to the console.

Options:
  --number INTEGER                The number of times to greet our friend.
  --greeting TEXT                 Word or phrase to use to greet our friend.
  --punctuation PUNCTUATION_MARK  Punctuation to use to end our greeting.
  --help                          Show this message and exit.
> ./hello_world.py --number five Ray
Usage: hello_world.py [OPTIONS] NAME_OF_OUR_FRIEND
Try 'hello_world.py --help' for help.

Error: Invalid value for '--number': 'five' is not a valid integer.
> ./hello_world.py --number 5 Ray
Hello, Ray!
Hello, Ray!
Hello, Ray!
Hello, Ray!
Hello, Ray!

Click gives types that are beyond the primitives like integer, string, etc. You can specify that an argument or option must be a file:

#!/usr/bin/env python3
import click


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our greeting.")
@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.argument('fh', metavar='FILE_WITH_LIST_OF_NAMES', type=click.File())
@click.command()
def hello_world(greeting, punctuation, fh):
    """
    Prints a polite, customized  greeting to the console.
    """
    for name in fh.readlines():
        click.echo(f'{greeting}, {name.strip()}{punctuation}')


if __name__ == '__main__':
    hello_world()

The user enters a path to a file for FILE_WITH_LIST_OF_NAMES argument, and click will automatically open the file and pass the handle into the command function. (By default, the file will be open for read, but you can pass other arguments to click.File() to open the file in other ways.) Click fails gracefully if it cannot open the file at the specified path.

> ./hello_world.py ./wrong_file.txt
Usage: hello_world.py [OPTIONS] FILE_WITH_LIST_OF_NAMES
Try 'hello_world.py --help' for help.

Error: Invalid value for 'FILE_WITH_LIST_OF_NAMES': './wrong_file.txt': No such file or directory
> ./hello_world.py ./names.txt
Hello, Ray!
Hello, Ben!
Hello, Julia!
Hello, Patrick!
Hello, Taylor!
Hello, David!

Click provides many useful types, and you can even implement your own custom types by subclassing click.ParamType.

Multiple and Nested Commands

Command Groups

The examples so far have included just a single command in our command-line tool, but you can implement several commands to create a more robust tool and richer command-line experience. We can use the click.group() decorator create a “command group”, and then assign several Click “commands” to that group. The main script invokes the command group rather than the command:

#!/usr/bin/env python3
import click


@click.group()
def hello_world():
    """
    Engage in a polite conversation with our friend.
    """
    pass


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our greeting.")
@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.argument('name', metavar='NAME_OF_OUR_FRIEND')
@hello_world.command()
def hello(name, greeting, punctuation):
    """
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{punctuation}')


@hello_world.command()
def goodbye():
    """
    Prints well-wishes for our departing friend.
    """
    click.echo('Goodbye, and safe travels!')


if __name__ == '__main__':
    hello_world()

In this example, we have moved the functionality of our greeting functionality to a command hello and implemented a new command goodbye. hello_world is now a command group containing these two commands. Click implements a help option for our command group much in the way that it implements them for commands:

> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] COMMAND [ARGS]...

  Engage in a polite conversation with our friend.

Options:
  --help  Show this message and exit.

Commands:
  goodbye  Prints well-wishes for our departing friend.
  hello    Prints a polite, customized greeting to the console.

The hello command preserves the options, arguments, and documentation that it had when it was the root command.

> ./hello_world.py hello --help
Usage: hello_world.py hello [OPTIONS] NAME_OF_OUR_FRIEND

  Prints a polite, customized  greeting to the console.

Options:
  --greeting TEXT                 Word or phrase to use to greet our friend.
  --punctuation PUNCTUATION_MARK  Punctuation to use to end our greeting.
  --help                          Show this message and exit.
> ./hello_world.py hello --punctuation . Ray
Hello, Ray.

Nesting

We can arbitrarily nest command groups and commands by putting command groups inside of other command groups:

#!/usr/bin/env python3
import click


@click.group()
def hello_world():
    """
    Engage in a polite conversation with our friend.
    """
    pass


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our greeting.")
@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.argument('name', metavar='NAME_OF_OUR_FRIEND')
@hello_world.command()
def hello(name, greeting, punctuation):
    """
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{punctuation}')


@hello_world.group(name='other-phrases')
def other():
    """
    Further conversation with our friend.
    """
    pass


@other.command()
def goodbye():
    """
    Prints well-wishes for our departing friend.
    """
    click.echo('Goodbye, and safe travels!')


@other.command(name='how-are-you')
def how():
    """
    Prints a polite inquiry into the well-being of our friend.
    """
    click.echo('How are you?')


if __name__ == '__main__':
    hello_world()
> ./hello_world.py --help
Usage: hello_world.py [OPTIONS] COMMAND [ARGS]...

  Engage in a polite conversation with our friend.

Options:
  --help  Show this message and exit.

Commands:
  hello          Prints a polite, customized greeting to the console.
  other-phrases  Further conversation with our friend.
> ./hello_world.py other-phrases --help
Usage: hello_world.py other-phrases [OPTIONS] COMMAND [ARGS]...

  Further conversation with our friend.

Options:
  --help  Show this message and exit.

Commands:
  goodbye      Prints well-wishes for our departing friend.
  how-are-you  Prints a polite inquiry into the well-being of our friend.
> ./hello_world.py other-phrases how-are-you
How are you?

Notice also that we can give our command groups and commands custom names, if we wish to name our commands and command groups something different from the Python functions that implement them.

Context

You can define options for command groups just as you can for commands. You can then use the Click context object to apply a command group’s options to its commands:

#!/usr/bin/env python3
import click


@click.option('--punctuation',
              default='!',
              metavar='PUNCTUATION_MARK',
              help="Punctuation to use to end our sentences.")
@click.group()
@click.pass_context
def hello_world(ctx, punctuation):
    """
    Engage in a polite conversation with our friend.
    """
    ctx.ensure_object(dict)
    ctx.obj['punctuation'] = punctuation
    pass


@click.option('--greeting',
              default='Hello',
              help="Word or phrase to use to greet our friend.")
@click.argument('name', metavar='NAME_OF_OUR_FRIEND')
@hello_world.command()
@click.pass_context
def hello(ctx, name, greeting):
    Prints a polite, customized  greeting to the console.
    """
    click.echo(f'{greeting}, {name}{ctx.obj["punctuation"]}')


@hello_world.group(name='other-phrases')
def other():
    """
    Further conversation with our friend.
    """
    pass


@other.command()
@click.pass_context
def goodbye(ctx):
    """
    Prints well-wishes for our departing friend.
    """
    click.echo(f'Goodbye, and safe travels{ctx.obj["punctuation"]}')


@other.command(name='how-are-you')
@click.pass_context
def how(ctx):
    """
    Prints a polite inquiry into the well-being of our friend.
    """
    click.echo(f'How are you{ctx.obj["punctuation"]}')


if __name__ == '__main__':
    hello_world()

Here, we initialize our context object to a dict, which we can then use to store and retrieve context values (such as the value for the punctuation option of the root command group).

./hello_world.py --help
Usage: hello_world.py [OPTIONS] COMMAND [ARGS]...

  Engage in a polite conversation with our friend.

Options:
  --punctuation PUNCTUATION_MARK  Punctuation to use to end our sentences.
  --help                          Show this message and exit.

Commands:
  hello          Prints a polite, customized greeting to the console.
  other-phrases  Further conversation with our friend
> ./hello_world.py --punctuation . hello Ray
Hello, Ray.
> ./hello_world.py --punctuation ?! other-phrases how-are-you
How are you?!

Conclusion

We are really only scratching the surface of what Click can do. For more, check out the Click documentation! I hope this helps you get started in building robust command-line interfaces.

(Feature photo by Sai Kiran Anagani on Unsplash)

Posted By