Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PyOhio 2016 - Let’s Make Better Command Line Applications

PyOhio 2016 - Let’s Make Better Command Line Applications

Ec6de98b75fa5831bc2f13564c5242fa?s=128

Dave Forgac

July 30, 2016
Tweet

Transcript

  1. Let's Make Better Command Line Applications

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

  3. Philosophy

  4. 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!
  5. 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.
  6. Working Together POSIX

  7. 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
  8. 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
  9. Pipelines ls -l | grep example | less Image by

    - en:File:Pipeline.svg, Public Domain, TyIzaeL https://commons.wikimedia.org/w/index.php?curid=11524651
  10. Why CLI?

  11. Why Python? Stdlib + PyPI “Readability counts.” (The Zen of

    Python) Argument parsing
  12. Why not Python?

  13. A Good Python Command Line App ...'s design is informed

    by The Zen of Python and The Unix Philosophy.
  14. Arguments & Options example ARGUMENT --long-option or -l --help --verbose

  15. std(in|out|err)

  16. Exit code

  17. Ctrl-c / Signals

  18. Configuration Standard location (per-platorm) Overridable

  19. Config Specificity Default value Config value Env variable CLI option

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

    disabled
  21. Parsers stdlib PyPI sys.argv getopt optparse argparse Cement Click Cliff

    Clint Compago Docopt Plac ...
  22. Boilerplate

  23. #!/usr/bin/python #! #!/usr/bin/env python or for Python 3 only: #!/usr/bin/env

    python3 not:
  24. Execute or Import def cli(): pass # do stuff here

    if __name__ == '__main__': cli()
  25. Package Layout pyohio ├── __init__.py ├── cli.py └── pyohio.py

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

    }, … )
  27. Argparse

  28. 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))
  29. 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
  30. 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!
  31. Click

  32. First: Decorators def wrapper(func): # wraps a function in order

    to add additional behavior @wrapper def my_function(example_arg): # does stuff
  33. 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))
  34. 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.
  35. 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!
  36. Docopt

  37. hello_docopt.py """Hello, Docopt. Usage: hello-docopt <name> hello-docopt [--count=<int>] <name> hello-docopt

    -h | --help Options: -h --help Show this screen. -c --count=<int> 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['<name>']))
  38. hello-docopt --help $ hello-docopt --help Hello, Docopt. Usage: hello-docopt <name>

    hello-docopt [--count=<int>] <name> hello-docopt -h | --help Options: -h --help Show this screen. -c --count=<int> Times to print greeting [default: 1].
  39. hello-docopt $ hello-docopt Usage: hello-docopt <name> hello-docopt [--count=<int>] <name> hello-docopt

    -h | --help $ hello-docopt PyOhio Hello, PyOhio! $ hello-docopt --count 3 PyOhio Hello, PyOhio! Hello, PyOhio! Hello, PyOhio!
  40. 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
  41. 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. …
  42. 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. """ …
  43. 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)
  44. 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.
  45. 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 …
  46. 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
  47. 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='<stdin>' encoding='utf-8'> Outfile: <unopened file 'results.txt' w> Counted characters... Done.
  48. Further Reading

  49. Packaging

  50. Cookiecutter

  51. Logging

  52. The Zen of Python + The Unix Philosophy = Better

    Python CLI
  53. Thank You! Notes, Examples, Slides, Feedback: Questions? dave@forgac.com @tylerdave http://daveops.com/pyohio2016