Argparse Shell

I recently built a toy project for Riak that allows me to tag files with tags and various other metadata. In the course of building it I continually thought that it would be nice to have a shell to issue commands in, perhaps even an extension of zsh that temporarily sets in scope functions, somewhat like virtualenv does. At the same time I came across argparse. It is a nice extension to OptionParser which I used a great deal, with included support for things like subcommands which fit nicely into the shell parsing area. It turns out that it worked perfectly as a simple parser.

Read/parse/print loop

The first step in setting up the shell was to use a loop to read user input. This is straightforward enough:

while True:
    s = raw_input("> ")
    ...

I opted to follow the argparse suggestion in the documentation and use set_defaults based on the subcommand issued. The final issue is that argparse exits upon parsing. The way I resolved this was simply to wrap the parsing in a SystemExit try/except. The better way to solve it is to subclass ArgumentParser and have exit() do nothing. I've shown this in the code sample below.

import argparse
import sys

class NoExitParser(argparse.ArgumentParser):
    def exit(self, *args, **kwargs):
        #skip the exit
        pass

def printer(parser, opts):
    #prints the arguments sent in after the command
    print ' '.join(opts.args)

def print_help(parser, opts):
    parser.print_help()

def exit(*args):
    sys.exit(0)

if sys.argv[1:]:
    parser = argparse.ArgumentParser()
else:
    parser = NoExitParser()

subparsers = parser.add_subparsers()

subparsers.add_parser('exit').set_defaults(func=exit)
subparsers.add_parser('help').set_defaults(func=print_help)
sp = subparsers.add_parser('printer')
sp.add_argument('args', nargs='+')
sp.set_defaults(func=printer)

def parse(s=None):
    opts = parser.parse_args(s)
    opts.func(parser, opts)

if not sys.argv[1:]:
    while True:
        s = raw_input("> ")
        try:
            parse(s.split(' '))
        except Exception, e:
            print "Error: ", str(e)

else:
    parse()

Most of the code is straightforward, as you are adding subparsers for each command that correspond to functions that can parse their own arguments. Subparsers can be built out further to have their own subparsers or complex arguments just like normal command lines. The nice thing about the above is that it is trivial to have code that can either take command line arguments or drop you into a shell to take multiple arguments, ideal for a tagger. Of course you could even make a shell command that would drop you into the shell.

Argparse makes for a nice, straightforward parsing engine for simple built in shells.