fancycmd.py
author Alexander Solovyov <piranha@piranha.org.ua>
Sat, 20 Jun 2009 17:00:54 +0300
changeset 12 f1590d2363ff
child 13 c1857739143c
permissions -rw-r--r--
intermediate commit for subcommand dispatcher

import sys, traceback, getopt

from fancyopts import parse

def help_(ui, header, cmdtable, globalopts, name=None):
    def helplist():
        hlp = {}
        for cmd, info in cmdtable.items():
            # TODO: Handle situation when there is no
            # commands starting with '^'!
            if name == 'shortlist' and not cmd.startswith('^'):
                continue # short help contains only marked commands
            cmd = cmd.lstrip('^')
            doc = info[0].__doc__ or '(no help text available)'
            hlp[cmd] = doc.splitlines()[0].rstrip()

        ui.status(header)
        hlp = sorted(hlp)
        maxlen = max(map(len, hlp))
        for cmd, doc in hlp.items():
            if ui.verbose:
                ui.write(' %s:\n     %s\n' % (cmd.replace('|', ', '), doc))
            else:
                ui.write(' %-*s  %s\n' % (maxlen, cmd.split('|', 1)[0], doc))

    if not cmdtable:
        ui.warn('No commands specified!\n')
        return

    if not name or name == 'shortlist':
        helplist()


def dispatch(args, cmdtable, globalopts=None):
    '''Dispatch command arguments based on subcommands.

     - ``args``: sys.argv[1:]
     - ``cmdtable``: dict of commands in next format::

     {'name': (function, options, usage)}

     - ``globalopts``: list of options which are applied to all
       commands, if not supplied will contain ``--help`` option

    where:

     - ``name`` is the name used on command-line. Can containt
       aliases (separate them with '|') or pointer to the fact
       that this command should be displayed in short help (start
       name with '^')
     - ``function`` is the actual callable
     - ``options`` is options list in fancyopts format
     - ``usage`` is the short string of usage
    '''

    ui = UI()
    if not globalopts:
        globalopts = [('h', 'help', False, 'display help')]

    try:
        return _dispatch(ui, args, cmdtable, globalopts + UI.options)
    except Abort, e:
        ui.warn('abort: %s' % e)
    except UnknownCommand, e:
        ui.warn("unknown command: '%s'" % e)
    except AmbiguousCommand, e:
        ui.warn("command '%s' is ambiguous:\n    %s\n" %
                (e.args[0], ' '.join(e.args[1])))
    except ParseError, e:
        if e.args[0]:
            ui.warn('%s: %s' % (e.args[0], e.args[1]))
            # display help here?
        else:
            ui.warn("%s\n" % e.args[1])
            help_(ui, 'Preved', cmdtable, globalopts, 'shortlist')
    except KeyboardInterrupt:
        ui.warn('interrupted!\n')
    except:
        ui.warn('unknown exception encountered')
        raise

    return -1

def _dispatch(ui, args, cmdtable, globalopts):
    cmd, func, args, options, globaloptions = cmdparse(args, cmdtable,
                                                       globalopts)

    ui.verbose = globaloptions['verbose']
    ui.quiet = globaloptions['quiet']

    if globaloptions['help']:
        pass # help
    elif not cmd:
        import ipdb; ipdb.set_trace()
        return help_(ui, '', cmdtable, globalopts, 'shortlist')

    try:
        return func(ui, *args, **options)
    except TypeError:
        if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
            raise ParseError(cmd, "invalid arguments")
        raise

def cmdparse(args, cmdtable, globalopts):
    # command is the first non-option
    cmd = None
    for arg in args:
        if not arg.startswith('-'):
            cmd = arg
            break

    if cmd:
        args.pop(args.index(cmd))

        aliases, info = findcmd(cmd, cmdtable)
        cmd = aliases[0]
        possibleargs = list(info[1])
    else:
        possibleargs = []

    possibleargs.extend(globalopts)

    try:
        options, args = parse(args, possibleargs)
    except getopt.GetoptError, e:
        raise ParseError(cmd, e)

    globaloptions = {}
    for o in globalopts:
        name = o[1]
        globaloptions[name] = options.pop(name)

    return (cmd, cmd and info[0] or None, args, options, globaloptions)

def findpossible(cmd, table):
    """
    Return cmd -> (aliases, command table entry)
    for each matching command.
    """
    choice = {}
    for e in table.keys():
        aliases = e.lstrip("^").split("|")
        found = None
        if cmd in aliases:
            found = cmd
        else:
            for a in aliases:
                if a.startswith(cmd):
                    found = a
                    break
        if found is not None:
            choice[found] = (aliases, table[e])

    return choice

def findcmd(cmd, table):
    """Return (aliases, command table entry) for command string."""
    choice = findpossible(cmd, table)

    if cmd in choice:
        return choice[cmd]

    if len(choice) > 1:
        clist = choice.keys()
        clist.sort()
        raise AmbiguousCommand(cmd, clist)

    if choice:
        return choice.values()[0]

    raise UnknownCommand(cmd)

class UI(object):
    '''User interface helper.

    Intended to ease handling of quiet/verbose output and more.

    You have three methods to handle program messages output:

      - ``UI.status`` is printed by default, but hidden with quiet option
      - ``UI.note`` is printed only if output is verbose
      - ``UI.write`` is printed in any case

    Additionally there is ``UI.warn`` method, which prints to stderr.
    '''

    options = [('v', 'verbose', False, 'enable additional output'),
               ('q', 'quiet', False, 'suppress output')]

    def __init__(self, verbose=False, quiet=False):
        self.verbose = verbose
        self.quiet = quiet

    def write(self, *messages):
        for m in messages:
            sys.stdout.write(m)

    def warn(self, *messages):
        for m in messages:
            sys.stderr.write(m)

    status = lambda self, *m: not self.quiet and self.write(*m)
    note = lambda self, *m: self.verbose and self.write(*m)

# Command exceptions
class CommandException(Exception):
    'Base class for command exceptions'

class Abort(CommandException):
    'Raised if an error in command occured'

class AmbiguousCommand(CommandException):
    'Raised if command is ambiguous'

class UnknownCommand(CommandException):
    'Raised if command is unknown'

class ParseError(CommandException):
    'Raised on error in command line parsing'

class SignatureError(CommandException):
    'Raised if function signature does not correspond to arguments'