Split generic subcommand code off its own module.
Use the code in git_cl.py, since it was the more evolved. Add documentation and clean up the structure along the way. This makes it possible to easily reuse the generic subcommand handling code. As a first step, only git_cl.py is using it. Eventually, gclient and gcl could be switch over. R=iannucci@chromium.org BUG= Review URL: https://chromiumcodereview.appspot.com/23250002 git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@218072 0039d316-1c4b-4281-b951-d872f2087c98experimental/szager/collated-output
parent
eec3ea3534
commit
0633fb4ff2
@ -0,0 +1,201 @@
|
||||
# Copyright 2013 The Chromium Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style license that can be
|
||||
# found in the LICENSE file.
|
||||
|
||||
"""Manages subcommands in a script.
|
||||
|
||||
Each subcommand should look like this:
|
||||
@usage('[pet name]')
|
||||
def CMDpet(parser, args):
|
||||
'''Prints a pet.
|
||||
|
||||
Many people likes pet. This command prints a pet for your pleasure.
|
||||
'''
|
||||
parser.add_option('--color', help='color of your pet')
|
||||
options, args = parser.parse_args(args)
|
||||
if len(args) != 1:
|
||||
parser.error('A pet name is required')
|
||||
pet = args[0]
|
||||
if options.color:
|
||||
print('Nice %s %d' % (options.color, pet))
|
||||
else:
|
||||
print('Nice %s' % pet)
|
||||
return 0
|
||||
|
||||
Explanation:
|
||||
- usage decorator alters the 'usage: %prog' line in the command's help.
|
||||
- docstring is used to both short help line and long help line.
|
||||
- parser can be augmented with arguments.
|
||||
- return the exit code.
|
||||
- Every function in the specified module with a name starting with 'CMD' will
|
||||
be a subcommand.
|
||||
- The module's docstring will be used in the default 'help' page.
|
||||
- If a command has no docstring, it will not be listed in the 'help' page.
|
||||
Useful to keep compatibility commands around or aliases.
|
||||
- If a command is an alias to another one, it won't be documented. E.g.:
|
||||
CMDoldname = CMDnewcmd
|
||||
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.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import sys
|
||||
|
||||
|
||||
def usage(more):
|
||||
"""Adds a 'usage_more' property to a CMD function."""
|
||||
def hook(fn):
|
||||
fn.usage_more = more
|
||||
return fn
|
||||
return hook
|
||||
|
||||
|
||||
def CMDhelp(parser, args):
|
||||
"""Prints list of commands or help for a specific command."""
|
||||
# This is the default help implementation. It can be disabled or overriden if
|
||||
# wanted.
|
||||
if not any(i in ('-h', '--help') for i in args):
|
||||
args = args + ['--help']
|
||||
_, args = parser.parse_args(args)
|
||||
# Never gets there.
|
||||
assert False
|
||||
|
||||
|
||||
class CommandDispatcher(object):
|
||||
def __init__(self, module):
|
||||
"""module is the name of the main python module where to look for commands.
|
||||
|
||||
The python builtin variable __name__ MUST be used for |module|. If the
|
||||
script is executed in the form 'python script.py', __name__ == '__main__'
|
||||
and sys.modules['script'] doesn't exist. On the other hand if it is unit
|
||||
tested, __main__ will be the unit test's module so it has to reference to
|
||||
itself with 'script'. __name__ always match the right value.
|
||||
"""
|
||||
self.module = sys.modules[module]
|
||||
|
||||
def enumerate_commands(self):
|
||||
"""Returns a dict of command and their handling function.
|
||||
|
||||
The commands must be in the '__main__' modules. To import a command from a
|
||||
submodule, use:
|
||||
from mysubcommand import CMDfoo
|
||||
|
||||
Automatically adds 'help' if not already defined.
|
||||
|
||||
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'))
|
||||
cmds.setdefault('help', CMDhelp)
|
||||
return cmds
|
||||
|
||||
def find_nearest_command(self, name):
|
||||
"""Retrieves the function to handle a command.
|
||||
|
||||
It automatically tries to guess the intended command by handling typos or
|
||||
incomplete names.
|
||||
"""
|
||||
commands = self.enumerate_commands()
|
||||
if name in commands:
|
||||
return commands[name]
|
||||
|
||||
# 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)]
|
||||
if len(commands_with_prefix) == 1:
|
||||
return commands[commands_with_prefix[0]]
|
||||
|
||||
# A #closeenough approximation of levenshtein distance.
|
||||
def close_enough(a, b):
|
||||
return difflib.SequenceMatcher(a=a, b=b).ratio()
|
||||
|
||||
hamming_commands = sorted(
|
||||
((close_enough(c, name), c) for c in commands),
|
||||
reverse=True)
|
||||
if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
|
||||
# Too ambiguous.
|
||||
return
|
||||
|
||||
if hamming_commands[0][0] < 0.8:
|
||||
# Not similar enough. Don't be a fool and run a random command.
|
||||
return
|
||||
|
||||
return commands[hamming_commands[0][1]]
|
||||
|
||||
def _add_command_usage(self, parser, command):
|
||||
"""Modifies an OptionParser object with the function's documentation."""
|
||||
name = command.__name__[3:]
|
||||
more = getattr(command, 'usage_more', '')
|
||||
if name == 'help':
|
||||
name = '<command>'
|
||||
# Use the module's docstring as the description for the 'help' command if
|
||||
# available.
|
||||
parser.description = self.module.__doc__
|
||||
else:
|
||||
# Use the command's docstring if available.
|
||||
parser.description = command.__doc__
|
||||
parser.description = (parser.description or '').strip()
|
||||
if parser.description:
|
||||
parser.description += '\n'
|
||||
parser.set_usage(
|
||||
'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more))
|
||||
|
||||
@staticmethod
|
||||
def _create_command_summary(name, command):
|
||||
"""Creates a oneline summary from the command's docstring."""
|
||||
if name != command.__name__[3:]:
|
||||
# Skip aliases.
|
||||
return ''
|
||||
doc = command.__doc__ or ''
|
||||
line = doc.split('\n', 1)[0].rstrip('.')
|
||||
if not line:
|
||||
return line
|
||||
return (line[0].lower() + line[1:]).strip()
|
||||
|
||||
def execute(self, parser, args):
|
||||
"""Dispatches execution to the right command.
|
||||
|
||||
Fallbacks to 'help' if not disabled.
|
||||
"""
|
||||
commands = self.enumerate_commands()
|
||||
length = max(len(c) for c in commands)
|
||||
|
||||
# Lists all the commands in 'help'.
|
||||
if commands['help']:
|
||||
docs = sorted(
|
||||
(name, self._create_command_summary(name, handler))
|
||||
for name, handler in commands.iteritems())
|
||||
# Skip commands without a docstring.
|
||||
commands['help'].usage_more = (
|
||||
'\n\nCommands are:\n' + '\n'.join(
|
||||
' %-*s %s' % (length, name, doc) for name, doc in docs if doc))
|
||||
|
||||
if args:
|
||||
if args[0] in ('-h', '--help') and len(args) > 1:
|
||||
# Inverse the argument order so 'tool --help cmd' is rewritten to
|
||||
# 'tool cmd --help'.
|
||||
args = [args[1], args[0]] + args[2:]
|
||||
command = self.find_nearest_command(args[0])
|
||||
if command:
|
||||
if command.__name__ == 'CMDhelp' and len(args) > 1:
|
||||
# Inverse the arguments order so 'tool help cmd' is rewritten to
|
||||
# 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work
|
||||
# too.
|
||||
args = [args[1], '--help'] + args[2:]
|
||||
command = self.find_nearest_command(args[0]) or command
|
||||
|
||||
# "fix" the usage and the description now that we know the subcommand.
|
||||
self._add_command_usage(parser, command)
|
||||
return command(parser, args[1:])
|
||||
|
||||
if commands['help']:
|
||||
# Not a known command. Default to help.
|
||||
self._add_command_usage(parser, commands['help'])
|
||||
return commands['help'](parser, args)
|
||||
|
||||
# Nothing can be done.
|
||||
return 2
|
||||
Loading…
Reference in New Issue