Slide 1

Slide 1 text

Writing (Better) CLI Applications with Python

Slide 2

Slide 2 text

Dave Forgac [email protected] @tylerdave Slides, Notes, Examples: http://daveops.com/fosscon2015

Slide 3

Slide 3 text

Why CLI?

Slide 4

Slide 4 text

Why Python? Stdlib + PyPI “Readability counts.” (The Zen of Python) Argument parsing

Slide 5

Slide 5 text

When not Python?

Slide 6

Slide 6 text

Good CLI? Behaves as expected

Slide 7

Slide 7 text

Arguments & Options example ARGUMENT --long-option or -l --help --verbose

Slide 8

Slide 8 text

std(in|out|err)

Slide 9

Slide 9 text

Exit code

Slide 10

Slide 10 text

Ctrl-c / Signals

Slide 11

Slide 11 text

Configuration

Slide 12

Slide 12 text

Colors!

Slide 13

Slide 13 text

Options stdlib PyPI sys.argv getopt optparse argparse Click Cliff Clint Compago Docopt Plac ...

Slide 14

Slide 14 text

Boilerplate

Slide 15

Slide 15 text

#! #!/usr/bin/env python or #!/usr/bin/env python2 or #!/usr/bin/env python3

Slide 16

Slide 16 text

Python 2 & 3 from __future__ import print_function

Slide 17

Slide 17 text

Execute or Import def main(): pass # do stuff here if __name__ == '__main__': main()

Slide 18

Slide 18 text

Python Package fosscon2015 ├── __init__.py ├── cli.py └── fosscon_stuff.py

Slide 19

Slide 19 text

Entry Points setup.py setup( … entry_points={ 'console_scripts':[ 'fosscon-argparse=fosscon2015.cli_argparse:cli', 'fosscon-click=fosscon2015.cli_click:cli', 'hello-argparse=fosscon2015.hello_argparse:hello', 'hello-click=fosscon2015.hello_click:hello', ], }, … )

Slide 20

Slide 20 text

argparse

Slide 21

Slide 21 text

hello_argparse.py import argparse def hello(): """ Argparse CLI to greet the provided NAME. """ parser = argparse.ArgumentParser() parser.add_argument('name', help="name of person to greet") parser.add_argument('-c', '--count', type=int, default=1, help="number of times to print the greeting") args = parser.parse_args() for i in xrange(args.count): print("Hello, {0}!".format(args.name))

Slide 22

Slide 22 text

hello-argparse --help $ hello-argparse --help usage: hello-argparse [-h] [-c COUNT] name positional arguments: name name of person to greet optional arguments: -h, --help show this help message and exit -c COUNT, --count COUNT number of times to print the greeting

Slide 23

Slide 23 text

hello-argparse $ hello-argparse usage: hello-argparse [-h] [-c COUNT] name hello-argparse: error: too few arguments $ hello-argparse FOSSCON Hello, FOSSCON! $ hello-argparse --count 3 FOSSCON Hello, FOSSCON! Hello, FOSSCON! Hello, FOSSCON!

Slide 24

Slide 24 text

Click

Slide 25

Slide 25 text

First: Decorators def wrapper(func): # wraps a function in order to add additional behavior @wrapper def my_function(example_arg): # does stuff

Slide 26

Slide 26 text

hello_click.py import click @click.command() @click.argument('name') @click.option('--count', '-c', default=1, help="Number of times to print the greeting") def hello(name, count): """ Click CLI to greet the provided NAME. """ for i in xrange(count): print("Hello, {0}!".format(name))

Slide 27

Slide 27 text

hello-click --help $ hello-click --help Usage: hello-click [OPTIONS] NAME Click CLI to greet the provided NAME. Options: -c, --count INTEGER Number of times to print the greeting --help Show this message and exit.

Slide 28

Slide 28 text

hello-click $ hello-click Usage: hello-click [OPTIONS] NAME Error: Missing argument "name". $ hello-click FOSSCON Hello, FOSSCON! $ hello-click --count 3 FOSSCON Hello, FOSSCON! Hello, FOSSCON! Hello, FOSSCON!

Slide 29

Slide 29 text

A better CLI app! Read from a file or stdin Write to a file or stdout Update to log or stderr Has an option or two Has colors! Does something

Slide 30

Slide 30 text

cli_click.py @click.command() @click.argument('infile', type=click.File('r'), default='-') @click.argument('outfile', type=click.File('w'), default='-') @click.option('--log-file', '-l', type=click.File('w'), default=sys.stderr) @click.option('--verbose', '-v', is_flag=True) def cli(infile, outfile, log_file, verbose): """ Count the occurances of characters in INFILE and output to OUTFILE. """ …

Slide 31

Slide 31 text

cli_click.py (cont'd) … if verbose: click.secho("Infile: {0}".format(infile), file=log_file) click.secho("Outfile: {0}".format(outfile), file=log_file) text = infile.read() char_counts = Counter(text) output = json.dumps(dict(char_counts.most_common()), indent=2) click.secho("Counted characters.", bold=True, file=log_file) click.secho(output, file=outfile, fg='green')

Slide 32

Slide 32 text

fosscon-click --help $ fosscon-click --help Usage: fosscon-click [OPTIONS] [INFILE] [OUTFILE] Count the occurances of characters in INFILE and output to OUTFILE. Options: -l, --log-file FILENAME -v, --verbose --help Show this message and exit.

Slide 33

Slide 33 text

File input, stdout output $ fosscon-click data/declaration.txt Counted characters... { "\n": 176, " ": 1706, "'": 1, "-": 3, ",": 104, ".": 37, "1": 1, "4": 1, …

Slide 34

Slide 34 text

stdin input, file output $ echo "testing" | fosscon-click - outfile.txt Counted characters... $ cat outfile.txt { "e": 1, "g": 1, "i": 1, "\n": 1, "n": 1, "s": 1, "t": 2 }

Slide 35

Slide 35 text

Verbose with log file $ echo "testing" | fosscon-click --verbose --log-file log.txt - out.txt $ cat log.txt Infile: <_io.TextIOWrapper name='' encoding='utf-8'> Outfile: Counted characters...

Slide 36

Slide 36 text

Further Reading

Slide 37

Slide 37 text

Virtualenv (I use virtualenvwrapper)

Slide 38

Slide 38 text

Cookiecutter

Slide 39

Slide 39 text

Logging

Slide 40

Slide 40 text

Packaging

Slide 41

Slide 41 text

Recap No Surprises DRY Explicit Decisions

Slide 42

Slide 42 text

Thank You! Talk & Contact Info: http://daveops.com/fosscon2015 [email protected] @tylerdave