16

What is the "cleanest" way to implement an command-line UI, similar to git's, for example:

git push origin/master
git remote add origin git://example.com master

Ideally also allowing the more flexible parsing, for example,

jump_to_folder app theappname v2
jump_to_folder app theappname source
jump_to_folder app theappname source v2
jump_to_folder app theappname build v1
jump_to_folder app theappname build 1
jump_to_folder app theappname v2 build

jump_to_folder is the scripts name, app is the command, theappname is a "fixed-location" parameter, "build" and "v2" etc are arguments (For example, possible arguments would be any number/any number prefixed with a v, or build/source/tmp/config)

I could just manually parse the arguments with a series of if/else/elifs, but there must be a more elegant way to do this?

As an entirely theoretically example, I could describe the UI schema..

app:
    fixed: application_name

    optional params:
        arg subsection:
            "build"
            "source"
            "tmp"
            "config"

        arg version:
            integer
            "v" + integer

Then parse the supplied arguments though the above schema, and get a dictionary:

>>> print schema.parse(["app", "theappname", "v1", "source"])
{
    "application_name": "theappname",
    "params":{
        "subsection": "source",
        "version":"v1"
    }
}

Does such a system exist? If not, how would I go about implementing something along these lines?

jfs
  • 399,953
  • 195
  • 994
  • 1,670
dbr
  • 165,801
  • 69
  • 278
  • 343

6 Answers6

17

argparse is perfect for this, specifically "sub-commands" and positional args

import argparse


def main():
    arger = argparse.ArgumentParser()

    # Arguments for top-level, e.g "subcmds.py -v"
    arger.add_argument("-v", "--verbose", action="count", default=0)

    subparsers = arger.add_subparsers(dest="command")

    # Make parser for "subcmds.py info ..."
    info_parser = subparsers.add_parser("info")
    info_parser.add_argument("-m", "--moo", dest="moo")

    # Make parser for "subcmds.py create ..."
    create_parser = subparsers.add_parser("create")
    create_parser.add_argument("name")
    create_parser.add_argument("additional", nargs="*")

    # Parse
    opts = arger.parse_args()

    # Print option object for debug
    print opts

    if opts.command == "info":
        print "Info command"
        print "--moo was %s" % opts.moo

    elif opts.command == "create":
        print "Creating %s" % opts.name
        print "Additional: %s" % opts.additional

    else:
        # argparse will error on unexpected commands, but
        # in case we mistype one of the elif statements...
        raise ValueError("Unhandled command %s" % opts.command)


if __name__ == '__main__':
    main()

This can be used like so:

$ python subcmds.py create myapp v1 blah
Namespace(additional=['v1', 'blah'], command='create', name='myapp', verbose=0)
Creating myapp
Additional: ['v1', 'blah']
$ python subcmds.py info --moo
usage: subcmds.py info [-h] [-m MOO]
subcmds.py info: error: argument -m/--moo: expected one argument
$ python subcmds.py info --moo 1
Namespace(command='info', moo='1', verbose=0)
Info command
--moo was 1
dbr
  • 165,801
  • 69
  • 278
  • 343
9

The cmd module would probably work well for this.

Example:

import cmd

class Calc(cmd.Cmd):
    def do_add(self, arg):
        print sum(map(int, arg.split()))

if __name__ == '__main__':
    Calc().cmdloop()

Run it:

$python calc.py
(Cmd) add 4 5
9
(Cmd) help

Undocumented commands:
======================
add  help

(Cmd)

See the Python docs or PyMOTW site for more info.

davidavr
  • 14,143
  • 4
  • 27
  • 31
2

Straight from one of my scripts:

import sys

def prog1_func1_act1(): print "pfa1"
def prog2_func2_act2(): print "pfa2"

commands = {
    "prog1 func1 act1": prog1_func1_act1,
    "prog2 func2 act2": prog2_func2_act2
}

try:
    commands[" ".join(sys.argv[1:])]()
except KeyError:
    print "Usage: ", commands.keys()

It's a pretty quick and dirty solution, but works great for my usage. If I were to clean it up a bit, I would probably add argparse to the mix for parsing positional and keyword arguments.

klozovin
  • 2,363
  • 2
  • 22
  • 30
  • The problem with this is the arguments are fixed.. For example, you couldn't easily have a "v001".."v102" option, short of creating a key for every combination.. – dbr Dec 12 '08 at 11:34
  • Yes I know, and that's why I mentioned argparse. For example, if you have relatively few "commands" and "actions", but a lot of parameters, you can dispatch to specific action with the approach I use, and then pass parameters (and optional keyword arguments) to your script that uses argparse. – klozovin Dec 12 '08 at 11:40
  • plus, you can add a meta-command 'help' that prints out the contents of commands and shows their .__doc__ strings – pjz Dec 12 '08 at 14:52
  • also, the args aren't really fixed because the commands in the command list take all the args, so they can be parse in each command (and common parsings can be factored out). Or, if you mean that the command list is limited, consider overriding __getkey__ in a commands object. – pjz Dec 12 '08 at 14:54
1

You might want to take a look at cliff – Command Line Interface Formulation Framework

Piotr Dobrogost
  • 41,292
  • 40
  • 236
  • 366
1

Python has a module for parsing command line options, optparse.

Egil
  • 5,600
  • 2
  • 32
  • 33
0

Here's my suggestion.

  1. Change your grammar slightly.

  2. Use optparse.

Ideally also allowing the more flexible parsing, for example,

jump_to_folder -n theappname -v2 cmd 
jump_to_folder -n theappname cmd source 
jump_to_folder -n theappname -v2 cmd source 
jump_to_folder -n theappname -v1 cmd build 
jump_to_folder -n theappname -1 cmd build 
jump_to_folder -n theappname -v2 cmd build

Then you have 1 or 2 args: the command is always the first arg. It's optional argument is always the second arg.

Everything else is options, in no particular order.

S.Lott
  • 384,516
  • 81
  • 508
  • 779