python Posts

Easy Command Line Apps with Argparse

argparse, which will be included in Python's Standard Library when 2.7 is released, is a great command line parsing library. It's both easier to use and more capable than optparse -- a fine combination! Here's a quick-and-dirty example of creating a script that supports commands, using argparse.

#!/usr/bin/env python
import sys

class Command(object):
    """Base class for commands.
    """
    # optional command-specific args
    args = []
    def execute(self, args):
        raise NotImplementedError()

def arg(*args, **kw):
    # sugar for arg declaration
    return (args, kw)

def commands():
    """Return list of Command sub classes from current module.
    """
    mod = sys.modules[__name__]
    classes = []
    for attr in [getattr(mod, name) for name in dir(mod)]:
        if Command in getattr(attr, '__bases__', []):
            classes.append(attr)
    return classes

def register_commands(arg_parser):
    """Register all subclasses of Command.
    """
    cmd_parsers = arg_parser.add_subparsers()
    for cmd in commands():
        # use first line of docstring as help
        help = (cmd.__doc__ or 'no help').strip().splitlines()[0]
        cmd_parser = cmd_parsers.add_parser(cmd.__name__, help=help)
        # add optional command-specific arguments
        for (args, kw) in cmd.args:
            cmd_parser.add_argument(*args, **kw)
        # This is really, really important!
        # Without it we won't know which command to execute.
        cmd_parser.set_defaults(command_class=cmd)

Define some commands:

class Date(Command):
    """show current date
    """
    args = [
        arg('format', default=['%Y-%m-%d'], nargs='*',
            help='strftime-compatiable format'),
    ]
    def execute(self, args):
        import datetime
        print datetime.date.today().strftime(' '.join(args.format))


class Echo(Command):
    """echo input to stdout
    """
    args = [
        arg('-n', dest='newline', default=True,
            action='store_false',
            help='do not output the trailing newline'),
        arg('strings', metavar='string', nargs='*'),
    ]
    def execute(self, args):
        sys.stdout.write(' '.join(args.strings))
        if args.newline:
            sys.stdout.write('\n')

Finally, glue it together with argparse.

if __name__ == '__main__':
    import argparse
    arg_parser = argparse.ArgumentParser()
    # really just to demonstrate a global option
    arg_parser.add_argument('-v', '--verbose',
        action='store_true', help='example global option')
    # register commands
    register_commands(arg_parser)
    # parse args including which command to run
    args = arg_parser.parse_args(sys.argv[1:])
    # create instance of command
    command = args.command_class()
    if args.verbose:
        print 'running', command
    # run it
    command.execute(args)

Let's try it! First, let's get a listing of commands:

orutherfurd@laptop:~/tmp$ ./argparse_commands.py --help
usage: argparse_commands.py [-h] {Date,Echo} ...

positional arguments:
  {Date,Echo}
    Date       show current date
    Echo       echo input to stdout

optional arguments:
  -h, --help     show this help message and exit
  -v, --verbose  example global option

then help for a specific one:

orutherfurd@laptop:~/tmp$ ./argparse_commands.py Echo --help
usage: argparse_commands.py Echo [-h] [-n] [string [string ...]]

positional arguments:
  string

optional arguments:
  -h, --help  show this help message and exit
  -n          do not output the trailing newline

lastly, let's run one:

orutherfurd@laptop:~/tmp$ ./argparse_commands.py Date
2010-04-29

orutherfurd@laptop:~/tmp$ ./argparse_commands.py Date %b %d, %Y
Apr 29, 2010

While cursory, this demonstrates how easy it is to create flexible command line apps using argparse. We supplied the Command boilerplate, but argparse took care of all argument parsing and made command dispatching a piece of cake.

Tagged with: python argparse

posted April 29th, 2010 at 4:19 p.m.