Docopt, the elegant way to build CLI

Recently, I’ve been caught in a troll with a friend who told me he doesn’t like docopt and prefers to stick with argparse.

My opinion is that both are just tools with strong and weak points. I do have a preference over defaulting to docopt, and use something else really when I’m hitting a use case that makes docopt totally irrelevent.

CLI Design

I don’t think there’s such a thing as a simple CLI or a complex CLI, because the number of arguments does not necessarily affects the complexity of the interface, the issue would mostly be how well is it exposed and documented so it is made easy to use.

I prefer to think in terms of usage. I believe there are two main approaches to CLI design. Either, it’s a complement to configuring the environment before starting a long running application (whether it is a service, a GUI, a webapp or an REPL), or it’s a tool meant to integrate in one user’s shell toolbox, where the CLI is the main interface.

KISS and DRY design

In the first case, your main focus is on the code itself, and not really on the arguments, so writing an argument handler and doing a start at documenting your app is done in a single action when you’re building your application.


'''
Usage: myapp [options]

Arguments:
  -c,--config=<file>       Sets up configuration file [default: myconf.yaml]
  -v,--verbose             Enable verbose output
  -h,--help		   This page

This is a tool to demonstrate that it takes less than a minute to setup a flexible,
readable and documented CLI for any kind of app.
'''

from docopt import docopt

def main():
   args = docopt(__doc__)

   # start your app with the default configurations
   my_app(
      config=args['--config'],
      verbose=args['--verbose']
   )

And then you can focus on actually coding whatever my_app() is doing, which is what you should be focused on. If you want to add a log option, you’d update only __doc__ with the matching line, and add it as a new argument in the my_app() call.

There your CLI parsing code is readable, showing only the essential, yet flexible enough to adapt to your needs so you’re always building up a CLI that makes the tool you’re building looking alike every other Unix tool.

More advanced CLI design

For small applications, checking arguments by looking at the dictionary is really nice way, because it keeps things simple and easy to read.

For more complex applications, where the CLI is the main point of interaction with the user, you will want to build an interface that has more arguments, and thus more possibilities in combining those arguments. And that’s ok, because some programs are meant to provide almost a “custom” language to make using them nice.

There, using docopt the traditional way will get your cyclometric complexity through the roof… like:

if args['command']:
   if args['--input']:
      if args['--accept']:
	 if args['--number']:
	    try:
	       n = int(args['--number'])
	    except:
	       print("Error!")
	 else:
	    # …

It’s actually the other side of the coin when having a very straightforward approach to CLI parsing, and it’s ok, because replacing a chaos of conditional with a more declarative approach is nothing new. You can write your own command dispatcher like I did for git-repo

Or be smarter than I was, and use the already existing docopt-dispatch which would deserve more credit:

from docopt_dispatch import dispatch

@dispatch.on('command', '--input', '--accept', '--number')
def do_command_input_accept_number(cmd, input, accept, number):
   try:
      n = int(number)
   except:
      print("Error!")

When docopt is not great, something’s wrong with your design

The only time I really reached the possibilities of what I could do with docopt was when I wanted to implement a configuration file that’s modifying how the CLI will behave.

It’s actually possible to do this with docopt, but you’ll end up having a configuration split in two parts and the second part being filled with {} format strings so that when reading the __doc__ or the source code, it’s becoming unreadable.

Doing so is definitely much simpler with something like argparse… But as I was trying to hack my way through that… I realized how wrong that was. The CLI should always override the configuration values, and the configuration override the default values. That’s the expected order of precendence, and doing it another way would be breaking an expected behaviour of any Unix software.

Docopt behaves the same way in other languages

When I needed to build a quick hack in C++ to test usage of a library, then the commandline is definitely not the important part. Back at uni, for that kind of code I used to either do it myself and make pretty ugly CLI, because I did not want to spend time on the parser (so basically iterating over the options from the end of the argv array), or use getopt.

Other options might be to use boost argument parser, or decide which one you want. And you don’t always get a great looking CLI help page.

With docopt.cpp, you can parse your code nicely. With a bit more complexity and increasing the size of your build… But it’s a thing you run only once at start of your app, and it’s still faster than spawning a process.

Yeah, I’m sticking with docopt

So, whatever better feature another argument parsing library has, I prefer to stick with docopt. Yes, it’s lacking type checking (though a patch tries to fix that), it’s not bundled with the language (but like many other great libraries like requests), and error reporting could be improved (another patch).

But what other tools lack is the flexibility to craft your own help message so it’s actually part of the documentation and not some ugly and hard to understand bit of technicality that forces you to go read the README before using the program.