Creating python command line applications with docopt

Introduction

Few weeks ago I wrote a blog post showing how we can write command line applications using argparse, you can check out it here. Some people suggested me to try docopt and this post is a result from this experience.

Docopt, the Pythonic command line arguments parser, that will make you smile

What is it ?

Docopt, like optparse and argparse, is a small library that simplifies the task to write command-line interfaces. But it came with a different idea.

How optparse and argparse work ? You have to read the documentation and write code that will provide guidelines to parse args. A example of this approach is the code that I wrote in my previous post. And being honest, I looked the documentation several times, trying to understand how each method works.

parser = argparse.ArgumentParser()
parser.add_argument("operation", help="mathematical operation that will be performed", 
	choices=['add', 'subtract', 'multiply', 'divide'])
parser.add_argument("num1", help="the first number", type=int)
parser.add_argument("num2", help="the second number", type=int)
args = parser.parse_args()

How docopt works ? When we are designing a command-line interface, we write or we should write, our application documentation, right ? Docopt uses this docstring to parse the args. Look at this example extracted from documentation:

"""Naval Fate.

Usage:
  naval_fate.py ship new <name>...
  naval_fate.py ship <name> move <x> <y> [--speed=<kn>]
  naval_fate.py ship shoot <x> <y>
  naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting]
  naval_fate.py -h | --help
  naval_fate.py --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=<kn>  Speed in knots [default: 10].
  --moored      Moored (anchored) mine.
  --drifting    Drifting mine.

"""
from docopt import docopt


if __name__ == '__main__':
    arguments = docopt(__doc__, version='Naval Fate 2.0')
    print(arguments)

When I saw it for the first time, I had a “holy shit” moment and said “It won’t work”, but it works! You will avoid to read documentation and to write parse code, all you have to do is writing your application documentation and instantly you have your application running with all options and help messages working properly!

Coming back to the calculator example

In my first post about command line interfaces I built two command calculators, one using python w/o libraries and other using argparse. So to compare these libraries, let’s build another calculator, in this time, using docopt. This is our documentation:

/*
We are going to create a command-line calculator. The calculator should be used in this way:

python calc.py add 2 2
> 4

python calc.py subtract 2 2
> 0

python calc.py multiply 2 2
> 4

python calc.py divide 2 2
> 1
   
*/

Let’s transform it in the docopt pattern, that is the pattern of almost all command-line applications.

"""Calculator using docopt

Usage:
  calc_docopt.py operation <num1> <num2>
  calc_docopt.py (-h | --help)

Arguments
  <operation> Math Operation
  <num1> First Number
  <num2> Second Number

Options:
  -h --help     Show this screen.

"""

Now, let’s add the docopt code, don’t be afraid of that. With 3-4 lines of code our parser are done.

"""Calculator using docopt

Usage:
  calc_docopt.py operation <num1> <num2>
  calc_docopt.py (-h | --help)

Arguments
  <operation> Math Operation
  <num1> First Number
  <num2> Second Number

Options:
  -h --help     Show this screen.

"""
from docopt import docopt

if __name__ == '__main__':
    arguments = docopt(__doc__, version='Calculator with docopt')
    print(arguments)

Help Messages

Let’s see some of the messages that docopt provide for us.

Help message

python calc_docopt.py -help
>>> Calculator using docopt
>>>
>>> Usage:
>>>   calc_docopt.py <operation> <num1> <num2>
>>>   calc_docopt.py (-h | --help)
>>> 
>>> Arguments
>>>   <operation> Math Operation
>>>   <num1> First Number
>>>   <num2> Second Number
>>> 
>>> Options:
>>>   -h --help     Show this screen.

Invalid Arguments

python calc_docopt.py
>>> Usage:
>>>   calc_docopt.py <operation> <num1> <num2>
>>>   calc_docopt.py (-h | --help)

Operations

Our work right now is to coding the calculator operations. I will respect the DRY’s gods and copy the operations from the other post.

"""Calculator using docopt

Usage:
  calc_docopt.py <operation> <num1> <num2>
  calc_docopt.py (-h | --help)

Arguments
  num1 First Number
  num2 Second Number

Options:
  -h --help     Show this screen.

"""
from docopt import docopt

if __name__ == '__main__':
    args = docopt(__doc__, version='Calculator with docopt')
    functions = {
        'add': lambda num1, num2: num1 + num2,
        'subtract': lambda num1, num2: num1 - num2,
        'multiply': lambda num1, num2: num1 * num2,
        'divide': lambda num1, num2: num1 / num2
    }

    print functions[args['<operation>']](int(args['<num1>']), int(args['<num2>']))

Our calculator now performs math operations as we expect.

Input Validation

If you are a good observer, you will see that our calculator lacks type parse/checking and operation checking. It’s easier to accomplish it with argparse. But docopt doesn’t have built-in input validations.

docopt does one thing and does it well: it implements your command-line interface. However it does not validate the input data. On the other hand there are libraries like python schema which make validating data a breeze.
- docopt documentation

You can get more info about python schema here.

Conclusion

Docopt is a new approach to write command apps. You can easily build apps. We can only appreciate Vladimir Keleshev’s work and hope that it continues to improving and why not conquest a place in python core ?

2 Comments

  1. Comment by Ted Stern:

    Docopt is great for simple command line interfaces. And yes, argparse can be hard to understand.

    However, argh wraps argparse in a powerful decorator-based interface that is as easy as docopt, without losing the power and built-in schema checking of argparse. Check it out.

Leave a Reply

Your email address will not be published. Required fields are marked *