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.
#!/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.@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
.#!/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
option to all commands (which can be disabled by passing add_help_option=False
to @click.command()
).#!/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.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 metavar
s:#!/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.
#!/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.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.
#!/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.#!/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?!
(Feature photo by Sai Kiran Anagani on Unsplash)
Posted by Raymond Lam