Slide 1

Slide 1 text

Let's Make Better Command Line Applications

Slide 2

Slide 2 text

Notes, Examples, Slides, Feedback: http://daveops.com/pyohio2016

Slide 3

Slide 3 text

Philosophy

Slide 4

Slide 4 text

The Zen of Python >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!

Slide 5

Slide 5 text

The Unix Philosophy Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.

Slide 6

Slide 6 text

Working Together POSIX

Slide 7

Slide 7 text

Standard Streams Text terminal Keyboard Display Program ① stdin ② stdout ③ stderr Image by - Own work, Public Domain, ScotXW https://commons.wikimedia.org/w/index.php?curid=687268

Slide 8

Slide 8 text

Redirection $ grep "Hello" * grep: example_directory: Is a directory example.txt:Hello, World! grep: protected-file.txt: Permission denied $ grep "Hello" * > matches.txt grep: example_directory: Is a directory grep: protected-file.txt: Permission denied $ cat matches.txt example.txt:Hello, World! $ grep "Hello" * > matches.txt 2> diagnostics.txt $ cat diagnostics.txt grep: example_directory: Is a directory grep: protected-file.txt: Permission denied

Slide 9

Slide 9 text

Pipelines ls -l | grep example | less Image by - en:File:Pipeline.svg, Public Domain, TyIzaeL https://commons.wikimedia.org/w/index.php?curid=11524651

Slide 10

Slide 10 text

Why CLI?

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Why not Python?

Slide 13

Slide 13 text

A Good Python Command Line App ...'s design is informed by The Zen of Python and The Unix Philosophy.

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

std(in|out|err)

Slide 16

Slide 16 text

Exit code

Slide 17

Slide 17 text

Ctrl-c / Signals

Slide 18

Slide 18 text

Configuration Standard location (per-platorm) Overridable

Slide 19

Slide 19 text

Config Specificity Default value Config value Env variable CLI option

Slide 20

Slide 20 text

Colors! Can be helpful ...when used correctly & can be disabled

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Boilerplate

Slide 23

Slide 23 text

#!/usr/bin/python #! #!/usr/bin/env python or for Python 3 only: #!/usr/bin/env python3 not:

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

Package Layout pyohio ├── __init__.py ├── cli.py └── pyohio.py

Slide 26

Slide 26 text

Entry Points setup.py setup( … entry_points={ 'console_scripts':[ 'hello-world=pyohio.cli:hello', 'pyohio=pyohio.cli:cli', ], }, … )

Slide 27

Slide 27 text

Argparse

Slide 28

Slide 28 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 29

Slide 29 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 30

Slide 30 text

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

Slide 31

Slide 31 text

Click

Slide 32

Slide 32 text

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

Slide 33

Slide 33 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 34

Slide 34 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 35

Slide 35 text

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

Slide 36

Slide 36 text

Docopt

Slide 37

Slide 37 text

hello_docopt.py """Hello, Docopt. Usage: hello-docopt hello-docopt [--count=] hello-docopt -h | --help Options: -h --help Show this screen. -c --count= Times to print greeting [default: 1]. """ from docopt import docopt def cli(): args = docopt(__doc__) for i in xrange(int(args['--count'])): print("Hello, {0}!".format(args['']))

Slide 38

Slide 38 text

hello-docopt --help $ hello-docopt --help Hello, Docopt. Usage: hello-docopt hello-docopt [--count=] hello-docopt -h | --help Options: -h --help Show this screen. -c --count= Times to print greeting [default: 1].

Slide 39

Slide 39 text

hello-docopt $ hello-docopt Usage: hello-docopt hello-docopt [--count=] hello-docopt -h | --help $ hello-docopt PyOhio Hello, PyOhio! $ hello-docopt --count 3 PyOhio Hello, PyOhio! Hello, PyOhio! Hello, PyOhio!

Slide 40

Slide 40 text

Let's make 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 41

Slide 41 text

Sample data Declaration of Independence [Adopted in Congress 4 July 1776] The Unanimous Declaration of the Thirteen United States of When, in the course of human events, it becomes necessary for dissolve the political bands which have connected them with an assume among the powers of the earth, the separate and equal s which the laws of nature and of nature's God entitle them, a d to the opinions of mankind requires that they should declare t which impel them to the separation. …

Slide 42

Slide 42 text

cli_click.py import click import sys from pyohio2016 import counter @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 char occurances in INFILE, output to OUTFILE. """ …

Slide 43

Slide 43 text

cli_click.py (cont'd) … if verbose: click.echo("Infile: {0}".format(infile), file=log_file) click.echo("Outfile: {0}".format(outfile), file=log_file) text = infile.read() char_counts = counter.count_chars(text) output = "\n".join( ["{0} {1}".format(k, v) for k, v in char_counts]) click.echo("Counted characters...", file=log_file) click.secho(output, file=outfile, fg='green') if verbose: click.echo("Done.".format(outfile), file=log_file)

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

File input, stdout output $ pyohio-click data/declaration.txt Counted characters... 1706 e 875 t 640 o 522 n 493 a 479 s 477 i 454 r 429 …

Slide 46

Slide 46 text

stdin input $ echo -n "test" | pyohio-click Counted characters... t 2 s 1 e 1 $ echo -n "test" | pyohio-click - results.txt Counted characters... $ cat results.txt t 2 s 1 e 1

Slide 47

Slide 47 text

Verbose with log file $ echo -n "test" | pyohio-click -v --log-file log.txt - out.txt $ cat out.txt t 2 s 1 e 1 $ cat log.txt Infile: <_io.TextIOWrapper name='' encoding='utf-8'> Outfile: Counted characters... Done.

Slide 48

Slide 48 text

Further Reading

Slide 49

Slide 49 text

Packaging

Slide 50

Slide 50 text

Cookiecutter

Slide 51

Slide 51 text

Logging

Slide 52

Slide 52 text

The Zen of Python + The Unix Philosophy = Better Python CLI

Slide 53

Slide 53 text

Thank You! Notes, Examples, Slides, Feedback: Questions? [email protected] @tylerdave http://daveops.com/pyohio2016