Formalizes support for '-' in command names.

This officially drops support for '_' in command names, which is ugly as hell.
'_' was very rarely used so it shouldn't affect too much users.

Refactor the code to be more readable.

R=stip@chromium.org
BUG=

Review URL: https://codereview.chromium.org/1040503003

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@294660 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/subcommand.py b/subcommand.py
index b6f8e5c..0201c90 100644
--- a/subcommand.py
+++ b/subcommand.py
@@ -37,6 +37,7 @@
     will result in oldname not being documented but supported and redirecting to
     newcmd. Make it a real function that calls the old function if you want it
     to be documented.
+  - CMDfoo_bar will be command 'foo-bar'.
 """
 
 import difflib
@@ -82,6 +83,11 @@
   return sys.modules.get('colorama') or sys.modules.get('third_party.colorama')
 
 
+def _function_to_name(name):
+  """Returns the name of a CMD function."""
+  return name[3:].replace('_', '-')
+
+
 class CommandDispatcher(object):
   def __init__(self, module):
     """module is the name of the main python module where to look for commands.
@@ -103,32 +109,31 @@
 
     Automatically adds 'help' if not already defined.
 
+    Normalizes '_' in the commands to '-'.
+
     A command can be effectively disabled by defining a global variable to None,
     e.g.:
       CMDhelp = None
     """
     cmds = dict(
-        (fn[3:], getattr(self.module, fn))
-        for fn in dir(self.module) if fn.startswith('CMD'))
+        (_function_to_name(name), getattr(self.module, name))
+        for name in dir(self.module) if name.startswith('CMD'))
     cmds.setdefault('help', CMDhelp)
     return cmds
 
-  def find_nearest_command(self, name):
-    """Retrieves the function to handle a command.
+  def find_nearest_command(self, name_asked):
+    """Retrieves the function to handle a command as supplied by the user.
 
-    It automatically tries to guess the intended command by handling typos or
-    incomplete names.
+    It automatically tries to guess the _intended command_ by handling typos
+    and/or incomplete names.
     """
-    # Implicitly replace foo-bar to foo_bar since foo-bar is not a valid python
-    # symbol but it's faster to type.
-    name = name.replace('-', '_')
     commands = self.enumerate_commands()
-    if name in commands:
-      return commands[name]
+    if name_asked in commands:
+      return commands[name_asked]
 
     # An exact match was not found. Try to be smart and look if there's
     # something similar.
-    commands_with_prefix = [c for c in commands if c.startswith(name)]
+    commands_with_prefix = [c for c in commands if c.startswith(name_asked)]
     if len(commands_with_prefix) == 1:
       return commands[commands_with_prefix[0]]
 
@@ -137,7 +142,7 @@
       return difflib.SequenceMatcher(a=a, b=b).ratio()
 
     hamming_commands = sorted(
-        ((close_enough(c, name), c) for c in commands),
+        ((close_enough(c, name_asked), c) for c in commands),
         reverse=True)
     if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
       # Too ambiguous.
@@ -153,8 +158,8 @@
     """Generates the short list of supported commands."""
     commands = self.enumerate_commands()
     docs = sorted(
-        (name, self._create_command_summary(name, handler))
-        for name, handler in commands.iteritems())
+        (cmd_name, self._create_command_summary(cmd_name, handler))
+        for cmd_name, handler in commands.iteritems())
     # Skip commands without a docstring.
     docs = [i for i in docs if i[1]]
     # Then calculate maximum length for alignment:
@@ -169,14 +174,14 @@
     return (
         'Commands are:\n' +
         ''.join(
-            '  %s%-*s%s %s\n' % (green, length, name, reset, doc)
-            for name, doc in docs))
+            '  %s%-*s%s %s\n' % (green, length, cmd_name, reset, doc)
+            for cmd_name, doc in docs))
 
   def _add_command_usage(self, parser, command):
     """Modifies an OptionParser object with the function's documentation."""
-    name = command.__name__[3:]
-    if name == 'help':
-      name = '<command>'
+    cmd_name = _function_to_name(command.__name__)
+    if cmd_name == 'help':
+      cmd_name = '<command>'
       # Use the module's docstring as the description for the 'help' command if
       # available.
       parser.description = (self.module.__doc__ or '').rstrip()
@@ -200,14 +205,15 @@
         parser.epilog = '\n' + parser.epilog.strip() + '\n'
 
     more = getattr(command, 'usage_more', '')
-    parser.set_usage(
-        'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more))
+    extra = '' if not more else ' ' + more
+    parser.set_usage('usage: %%prog %s [options]%s' % (cmd_name, extra))
 
   @staticmethod
-  def _create_command_summary(name, command):
-    """Creates a oneline summary from the command's docstring."""
-    if name != command.__name__[3:]:
-      # Skip aliases.
+  def _create_command_summary(cmd_name, command):
+    """Creates a oneliner summary from the command's docstring."""
+    if cmd_name != _function_to_name(command.__name__):
+      # Skip aliases. For example using at module level:
+      # CMDfoo = CMDbar
       return ''
     doc = command.__doc__ or ''
     line = doc.split('\n', 1)[0].rstrip('.')