Your python code is now 100% more elegant and usable as a command-line program using Click

Many people who use python are used to the scriptfile workflow, where you write a rigid script in python and run it, somewhat similar to how people use bash scripts.

  1. You find an answer in a blog somewhere that describes how to do something in python
  2. You draft up a python script for the task and get it to work
  3. You hard-code all the parameters the script needs

Don't get me wrong – drafting up python scripts to prototype your idea is really useful. You get your idea working fast, even if your code becomes messy and unmaintainable.

However, wouldn't you like your python script to be as elegant and usable as all the command-line utilities you're used to?

1
2
3
4
ls -l
find -name *.py
grep -r "some text"
ssh --version

For kicks, lets take the python3 print() function, and give it a CLI. Kind of like the echo command in bash.

This is the definition of print(): https://docs.python.org/3/library/functions.html#print

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

So in python, you can print in all sorts of ways, more than just print('hello'). For example, you can add customized string to append to the end, change the seperator between words, and output to a file instead of stdout.

1
2
3
print('hello', end='\r') # brings cursor back to beginning of line
print('hello', 'hi', sep=',') # prints commas between each word
print('hello', file=open('out.txt', 'w')) # write output to file

The simple CLI

Take a look on how easy it is to turn the print statement above into a full-featured CLI.

print.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3
import click
import sys

@click.command()
@click.argument('args', nargs=-1)
@click.option('-s', '--sep', default=' ', help='String to seperate each argument (default: " ")]')
@click.option('-e', '--end', default='\n', help='String to append to end (default: "\\n")')
@click.option('-o', '--out', default=None, help='Output file (default: stdout)')
def print_prog(args, sep, end, out):
'''Print the arguments you give this program to stdout or to a file. Similar to echo.
'''
print(*args, sep=sep, end=end, file=open(out, 'w') if out else sys.stdout)

if __name__ == '__main__':
print_prog()

Essentially this script is taking all the command line arguments and giving them directly to the print() function. So we are in effect giving the bash-terminal all the capabilities of python's print(). Instead of print this CLI program could easily be changed to perform any scripting task with flexible and configurable input.

Now that we have our command-line-enabled python script, lets see how well it does…

echoing things

1
./print.py can it echo this?
can it echo this?

displaying help menu

1
./print.py --help
Usage: print.py [OPTIONS] [ARGS]...

  Print the arguments you give this program to stdout or to a file. Similar
  to echo.

Options:
  -s, --sep TEXT  String to seperate each argument (default: " ")]
  -e, --end TEXT  String to append to end (default: "\n")
  -o, --out TEXT  Output file (default: stdout)
  --help          Show this message and exit.

Wow… we can already see this has more features than the ever-so-useful echo

For example, we can change the seperator between the args

1
./print.py these args are seperated by dashes --sep='-'
these-args-are-seperated-by-dashes

Also, not always ending with just a newline

1
./print.py maybe I want 2 newlines --end=$'\n\n' && print.py instead
maybe I want 2 newlines

instead

You can write to a file too

1
./print.py write this to a file --out=file.txt

Then read the file

1
cat file.txt
write this to a file

Making this your own

This was a demonstration to show you how easy it is to create your own CLI. You don't need write plain python scriptfiles anymore.

Take this template and apply it to your own program.

Visit the Click website to reference the Click documentation while building your projects.

Also, for a slightly more complex example, you can check out my QuadArt project which uses Click CLI to be usable by anyone through the terminal. I wrote a blog post about it here. Here is an exerpt of the part of quadart.py that uses Click:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@click.command()
@click.argument('filename')
@click.option('-l', '--left', default=None, help='left pixel of image')
@click.option('-r', '--right', default=None, help='right pixel of image')
@click.option('-u', '--up', default=None, help='top pixel of image')
@click.option('-d', '--down', default=None, help='bottom pixel of image')
@click.option('-o', '--output', default=None, help='name of file to save result to')
@click.option('-s', '--size', default=512, help='Output size')
@click.option('-t', '--type', 'draw_type', default='circle', help='Draw type')
@click.option('--thresh', default=10, help='Standard deviation threshold for color difference')
@click.option('-m', '--max-recurse', 'max_recurse', default=0, help='Maximum allowed recursion depth. Default is infinity.')
def main(filename, left, right, up, down, output, size, draw_type, thresh, max_recurse):
quadart = QuadArt(std_thresh=thresh, draw_type=draw_type, max_recurse=max_recurse)
quadart.generate(filename, left=left, right=right,
up=up, down=down,
output_size=size)
if output is None:
quadart.display()
else:
quadart.save(output)

if __name__ == '__main__':
main()

This is the magic of combining python and the terminal with the glue of CLI. The possibilities are endless!

See you next post.

–Ricky

Comments