[PATCH] [RFC] Allow multiple levels of command dispatching

Bryan O'Sullivan bos at serpentine.com
Mon Apr 14 16:35:10 CDT 2008


# HG changeset patch
# User Bryan O'Sullivan <bos at serpentine.com>
# Date 1208208827 25200
# Node ID 4de599e16c27964ad12c227b5dee1f41cb2ff671
# Parent  2af1b9de62b34728b2ebe482ac2d36c1beab3b62
Allow multiple levels of command dispatching

This makes it possible to create a "master" command with several
subcommands, so one can run e.g. "hg module add".

This patch contains a single example command, "module add".

diff -r 2af1b9de62b3 -r 4de599e16c27 hgext/alias.py
--- a/hgext/alias.py	Mon Apr 14 23:04:34 2008 +0200
+++ b/hgext/alias.py	Mon Apr 14 14:33:47 2008 -0700
@@ -42,7 +42,7 @@
             return
 
         try:
-            self._cmd = findcmd(self._ui, self._target, commands.table)[1]
+            self._cmd = findcmd(self._ui, [self._target], commands.table)[2]
             if self._cmd == self:
                 raise RecursiveCommand()
             if self._target in commands.norepo.split(' '):
diff -r 2af1b9de62b3 -r 4de599e16c27 hgext/keyword.py
--- a/hgext/keyword.py	Mon Apr 14 23:04:34 2008 +0200
+++ b/hgext/keyword.py	Mon Apr 14 14:33:47 2008 -0700
@@ -417,9 +417,10 @@
             kwtools['exc'].append(pat)
 
     if kwtools['inc']:
-        def kwdispatch_parse(ui, args):
+        def kwdispatch_parse(ui, args, table):
             '''Monkeypatch dispatch._parse to obtain running hg command.'''
-            cmd, func, args, options, cmdoptions = dispatch_parse(ui, args)
+            cmd, func, args, options, cmdoptions = dispatch_parse(ui, args,
+                                                                  table)
             kwtools['hgcmd'] = cmd
             return cmd, func, args, options, cmdoptions
 
diff -r 2af1b9de62b3 -r 4de599e16c27 mercurial/cmdutil.py
--- a/mercurial/cmdutil.py	Mon Apr 14 23:04:34 2008 +0200
+++ b/mercurial/cmdutil.py	Mon Apr 14 14:33:47 2008 -0700
@@ -46,22 +46,32 @@
 
     return choice
 
-def findcmd(ui, cmd, table):
-    """Return (aliases, command table entry) for command string."""
-    choice = findpossible(ui, cmd, table)
+def findcmd(ui, args, table):
+    "Return (aliases, command, command table entry, args) for command string."
+    trail = []
+    for i in xrange(len(args)):
+        cmd = args[i]
+        choice = findpossible(ui, cmd, table)
 
-    if cmd in choice:
-        return choice[cmd]
+        entry = None
 
-    if len(choice) > 1:
-        clist = choice.keys()
-        clist.sort()
-        raise AmbiguousCommand(cmd, clist)
+        if cmd in choice:
+            aliases, entry = choice[cmd]
+        elif len(choice) > 1:
+            clist = choice.keys()
+            clist.sort()
+            raise AmbiguousCommand(' '.join(args[:i+1]), clist)
+        elif choice:
+            aliases, entry = choice.values()[0]
+        else:
+            raise UnknownCommand(' '.join(args[:i+1]))
 
-    if choice:
-        return choice.values()[0]
-
-    raise UnknownCommand(cmd)
+        trail.append(aliases[0])
+        if isinstance(entry[0], dict):
+            table = entry[0]
+        else:
+            return aliases, trail, entry, args[i+1:]
+    return [trail[-1]], trail, entry, args[i+1:]
 
 def bail_if_changed(repo):
     if repo.dirstate.parents()[1] != nullid:
diff -r 2af1b9de62b3 -r 4de599e16c27 mercurial/commands.py
--- a/mercurial/commands.py	Mon Apr 14 23:04:34 2008 +0200
+++ b/mercurial/commands.py	Mon Apr 14 14:33:47 2008 -0700
@@ -11,7 +11,7 @@
 import os, re, sys, urllib
 import hg, util, revlog, bundlerepo, extensions, copies
 import difflib, patch, time, help, mdiff, tempfile
-import version, socket
+import dispatch, version, socket
 import archival, changegroup, cmdutil, hgweb.server, sshserver, hbisect
 import merge as merge_
 
@@ -609,14 +609,14 @@
     a = r.ancestor(lookup(rev1), lookup(rev2))
     ui.write("%d:%s\n" % (r.rev(a), hex(a)))
 
-def debugcomplete(ui, cmd='', **opts):
+def debugcomplete(ui, *cmds, **opts):
     """returns the completion list associated with the given command"""
 
     if opts['options']:
         options = []
         otables = [globalopts]
-        if cmd:
-            aliases, entry = cmdutil.findcmd(ui, cmd, table)
+        if cmds:
+            entry = cmdutil.findcmd(ui, cmds, table)[2]
             otables.append(entry[1])
         for t in otables:
             for o in t:
@@ -626,7 +626,21 @@
         ui.write("%s\n" % "\n".join(options))
         return
 
-    clist = cmdutil.findpossible(ui, cmd, table).keys()
+    if not cmds:
+        clist = cmdutil.findpossible(ui, '', table).keys()
+    else:
+        cmdtable = table
+        for cmd in cmds:
+            c = cmdutil.findpossible(ui, cmd, cmdtable)
+            clist = c.keys()
+            if not c or len(c) > 1:
+                break
+            ent = c.values()[0][1][0]
+            if isinstance(ent, dict):
+                cmdtable = ent
+            else:
+                break
+            
     clist.sort()
     ui.write("%s\n" % "\n".join(clist))
 
@@ -1213,7 +1227,7 @@
     for n in heads:
         displayer.show(changenode=n)
 
-def help_(ui, name=None, with_version=False):
+def help_(ui, *names, **opts):
     """show help for a command, extension, or list of commands
 
     With no arguments, print a list of commands and short help.
@@ -1224,42 +1238,51 @@
     commands it provides."""
     option_lists = []
 
+    pname = ' '.join(names)
+
     def addglobalopts(aliases):
         if ui.verbose:
             option_lists.append((_("global options:"), globalopts))
-            if name == 'shortlist':
+            if names == ('shortlist',):
                 option_lists.append((_('use "hg help" for the full list '
                                        'of commands'), ()))
         else:
-            if name == 'shortlist':
+            if names == ('shortlist',):
                 msg = _('use "hg help" for the full list of commands '
                         'or "hg -v" for details')
             elif aliases:
                 msg = _('use "hg -v help%s" to show aliases and '
-                        'global options') % (name and " " + name or "")
+                        'global options') % (pname and " " + pname or "")
             else:
-                msg = _('use "hg -v help %s" to show global options') % name
+                msg = _('use "hg -v help %s" to show global options') % pname
             option_lists.append((msg, ()))
 
-    def helpcmd(name):
-        if with_version:
+    def helpcmd(*names):
+        if opts.get('with_version'):
             version_(ui)
             ui.write('\n')
-        aliases, i = cmdutil.findcmd(ui, name, table)
+        aliases, cmd, i = cmdutil.findcmd(ui, names, table)[:3]
         # synopsis
         ui.write("%s\n" % i[2])
 
         # aliases
         if not ui.quiet and len(aliases) > 1:
-            ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:]))
+            if len(cmd) > 1:
+                prefix = ' '.join(cmd) + ' '
+            else:
+                prefix = ''
+            ui.write(_("\naliases: %s%s\n") % (prefix, ', '.join(aliases[1:])))
 
-        # description
-        doc = i[0].__doc__
-        if not doc:
-            doc = _("(No help text available)")
-        if ui.quiet:
-            doc = doc.splitlines(0)[0]
-        ui.write("\n%s\n" % doc.rstrip())
+        if isinstance(i[0], dict):
+            helplist(_('list of subcommands:\n\n'), cmdtable=i[0])
+        else:
+            # description
+            doc = i[0].__doc__
+            if not doc:
+                doc = _("(No help text available)")
+            if ui.quiet:
+                doc = doc.splitlines(0)[0]
+            ui.write("\n%s\n" % doc.rstrip())
 
         if not ui.quiet:
             # options
@@ -1268,23 +1291,33 @@
 
             addglobalopts(False)
 
-    def helplist(header, select=None):
+    def helplist(header, select=None, cmdtable=table):
         h = {}
         cmds = {}
-        for c, e in table.items():
-            f = c.split("|", 1)[0]
-            if select and not select(f):
-                continue
-            if name == "shortlist" and not f.startswith("^"):
-                continue
-            f = f.lstrip("^")
-            if not ui.debugflag and f.startswith("debug"):
-                continue
-            doc = e[0].__doc__
-            if not doc:
-                doc = _("(No help text available)")
-            h[f] = doc.splitlines(0)[0].rstrip()
-            cmds[f] = c.lstrip("^")
+        def findcmds(prefix, table):
+            for c, e in table.iteritems():
+                f = c.split("|", 1)[0]
+                if select and not select(f):
+                    continue
+                if names == ('shortlist',) and not f.startswith("^"):
+                    continue
+                f = f.lstrip("^")
+                if not ui.debugflag and f.startswith("debug"):
+                    continue
+                f = prefix + f
+                if isinstance(e[0], dict):
+                    if ui.verbose:
+                        findcmds(f + ' ', e[0])
+                        continue
+                    else:
+                        doc = e[2]
+                else:
+                    doc = e[0].__doc__
+                if not doc:
+                    doc = _("(No help text available)")
+                h[f] = doc.splitlines(0)[0].rstrip()
+                cmds[f] = prefix + c.lstrip("^")
+        findcmds('', cmdtable)
 
         if not h:
             ui.status(_('no commands defined\n'))
@@ -1343,13 +1376,13 @@
             ct = {}
 
         modcmds = dict.fromkeys([c.split('|', 1)[0] for c in ct])
-        helplist(_('list of commands:\n\n'), modcmds.has_key)
+        helplist(_('list of commands:\n\n'), select=modcmds.has_key)
 
-    if name and name != 'shortlist':
+    if names and names != ('shortlist',):
         i = None
         for f in (helpcmd, helptopic, helpext):
             try:
-                f(name)
+                f(*names)
                 i = None
                 break
             except cmdutil.UnknownCommand, inst:
@@ -1359,14 +1392,14 @@
 
     else:
         # program name
-        if ui.verbose or with_version:
+        if ui.verbose or opts.get('with_version'):
             version_(ui)
         else:
             ui.status(_("Mercurial Distributed SCM\n"))
         ui.status('\n')
 
         # list of commands
-        if name == "shortlist":
+        if names == ('shortlist',):
             header = _('basic commands:\n\n')
         else:
             header = _('list of commands:\n\n')
@@ -1895,6 +1928,14 @@
                                'use "hg update" or merge with an explicit rev'))
         node = parent == heads[0] and heads[-1] or heads[0]
     return hg.merge(repo, node, force=force)
+
+def module_add(ui, repo, mod):
+    "floom!"
+    print mod
+
+module_table = {
+    "add": (module_add, [], _('hg module add [OPTION] MOD')),
+    }
 
 def outgoing(ui, repo, dest=None, **opts):
     """show changesets not found in destination
@@ -3179,6 +3220,8 @@
           ('r', 'rev', '', _('revision to merge')),
              ],
          _('hg merge [-f] [[-r] REV]')),
+    "module":
+        (module_table, [], _('module management commands\n')),
     "outgoing|out":
         (outgoing,
          [('f', 'force', None,
diff -r 2af1b9de62b3 -r 4de599e16c27 mercurial/dispatch.py
--- a/mercurial/dispatch.py	Mon Apr 14 23:04:34 2008 +0200
+++ b/mercurial/dispatch.py	Mon Apr 14 14:33:47 2008 -0700
@@ -158,7 +158,7 @@
 
     return p
 
-def _parse(ui, args):
+def _parse(ui, args, table):
     options = {}
     cmdoptions = {}
 
@@ -168,10 +168,8 @@
         raise ParseError(None, inst)
 
     if args:
-        cmd, args = args[0], args[1:]
-        aliases, i = cmdutil.findcmd(ui, cmd, commands.table)
-        cmd = aliases[0]
-        defaults = ui.config("defaults", cmd)
+        cmd, i, args = cmdutil.findcmd(ui, args, table)[1:]
+        defaults = ui.config("defaults", ' '.join(cmd))
         if defaults:
             args = shlex.split(defaults) + args
         c = list(i[1])
@@ -186,7 +184,7 @@
     try:
         args = fancyopts.fancyopts(args, c, cmdoptions)
     except fancyopts.getopt.GetoptError, inst:
-        raise ParseError(cmd, inst)
+        raise ParseError(' '.join(cmd), inst)
 
     # separate global options back out
     for o in commands.globalopts:
@@ -296,7 +294,8 @@
         util._fallbackencoding = fallback
 
     fullargs = args
-    cmd, func, args, options, cmdoptions = _parse(lui, args)
+    cmd, func, args, options, cmdoptions = _parse(lui, args, commands.table)
+    tcmd = ' '.join(cmd or [])
 
     if options["config"]:
         raise util.Abort(_("Option --config may not be abbreviated!"))
@@ -328,14 +327,15 @@
                  not options["noninteractive"], options["traceback"])
 
     if options['help']:
-        return commands.help_(ui, cmd, options['version'])
+        return commands.help_(ui, with_version=options['version'],
+                              *(cmd or []))
     elif options['version']:
         return commands.version_(ui)
     elif not cmd:
         return commands.help_(ui, 'shortlist')
 
     repo = None
-    if cmd not in commands.norepo.split():
+    if tcmd not in commands.norepo.split():
         try:
             repo = hg.repository(ui, path=path)
             ui = repo.ui
@@ -343,7 +343,7 @@
                 raise util.Abort(_("repository '%s' is not local") % path)
             ui.setconfig("bundle", "mainreporoot", repo.root)
         except RepoError:
-            if cmd not in commands.optionalrepo.split():
+            if tcmd not in commands.optionalrepo.split():
                 if args and not path: # try to infer -R from command args
                     repos = map(_findrepo, args)
                     guess = repos[0]
@@ -358,13 +358,14 @@
         d = lambda: func(ui, *args, **cmdoptions)
 
     # run pre-hook, and abort if it fails
-    ret = hook.hook(lui, repo, "pre-%s" % cmd, False, args=" ".join(fullargs))
+    ret = hook.hook(lui, repo, "pre-%s" % '-'.join(cmd), False,
+                    args=" ".join(fullargs))
     if ret:
         return ret
     ret = _runcommand(ui, options, cmd, d)
     # run post-hook, passing command result
-    hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
-              result = ret)
+    hook.hook(lui, repo, "post-%s" % '-'.join(cmd), False,
+              args=" ".join(fullargs), result=ret)
     return ret
 
 def _runcommand(ui, options, cmd, cmdfunc):
@@ -376,9 +377,9 @@
             tb = traceback.extract_tb(sys.exc_info()[2])
             if len(tb) != 2: # no
                 raise
-            raise ParseError(cmd, _("invalid arguments"))
+            raise ParseError(' '.join(cmd), _("invalid arguments"))
 
-    if options['profile']:
+    if options.get('profile'):
         import hotshot, hotshot.stats
         prof = hotshot.Profile("hg.prof")
         try:
@@ -397,7 +398,7 @@
             stats.strip_dirs()
             stats.sort_stats('time', 'calls')
             stats.print_stats(40)
-    elif options['lsprof']:
+    elif options.get('lsprof'):
         try:
             from mercurial import lsprof
         except ImportError:
diff -r 2af1b9de62b3 -r 4de599e16c27 tests/test-debugcomplete.out
--- a/tests/test-debugcomplete.out	Mon Apr 14 23:04:34 2008 +0200
+++ b/tests/test-debugcomplete.out	Mon Apr 14 14:33:47 2008 -0700
@@ -25,6 +25,7 @@
 log
 manifest
 merge
+module
 outgoing
 parents
 paths
diff -r 2af1b9de62b3 -r 4de599e16c27 tests/test-globalopts.out
--- a/tests/test-globalopts.out	Mon Apr 14 23:04:34 2008 +0200
+++ b/tests/test-globalopts.out	Mon Apr 14 14:33:47 2008 -0700
@@ -175,6 +175,7 @@
  log          show revision history of entire repository or files
  manifest     output the current or given revision of the project manifest
  merge        merge working directory with another revision
+ module add   (No help text available)
  outgoing     show changesets not found in destination
  parents      show the parents of the working dir or revision
  paths        show definition of symbolic path names
@@ -229,6 +230,7 @@
  log          show revision history of entire repository or files
  manifest     output the current or given revision of the project manifest
  merge        merge working directory with another revision
+ module add   (No help text available)
  outgoing     show changesets not found in destination
  parents      show the parents of the working dir or revision
  paths        show definition of symbolic path names
diff -r 2af1b9de62b3 -r 4de599e16c27 tests/test-help.out
--- a/tests/test-help.out	Mon Apr 14 23:04:34 2008 +0200
+++ b/tests/test-help.out	Mon Apr 14 14:33:47 2008 -0700
@@ -66,6 +66,7 @@
  log          show revision history of entire repository or files
  manifest     output the current or given revision of the project manifest
  merge        merge working directory with another revision
+ module add   (No help text available)
  outgoing     show changesets not found in destination
  parents      show the parents of the working dir or revision
  paths        show definition of symbolic path names
@@ -116,6 +117,7 @@
  log          show revision history of entire repository or files
  manifest     output the current or given revision of the project manifest
  merge        merge working directory with another revision
+ module add   (No help text available)
  outgoing     show changesets not found in destination
  parents      show the parents of the working dir or revision
  paths        show definition of symbolic path names


More information about the Mercurial-devel mailing list