Chromium Code Reviews| Index: subcommand.py |
| diff --git a/subcommand.py b/subcommand.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..bc8d39ccdb7d7acce710f8bfa7706efb1c89d429 |
| --- /dev/null |
| +++ b/subcommand.py |
| @@ -0,0 +1,179 @@ |
| +# Copyright (c) 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 alter the usage: 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 main module with a name starting with 'CMD' will be a |
| + subcommand. |
| + - The module's docstring will be used in the default help page. |
| +""" |
| + |
| +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='__main__'): |
| + """module is the main python module where to search for commands.""" |
| + self.module_name = module |
|
iannucci
2013/08/15 21:18:52
Why not
def __init__(self, module=None):
self.m
|
| + |
| + @property |
| + def module(self): |
| + return sys.modules.get(self.module_name) or sys.modules.get('__main__') |
|
M-A Ruel
2013/08/15 21:08:27
This is tricky because:
- 'git_cl' is not present
|
| + |
| + def get_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 get_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.get_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 __main__ module docstring as the help. |
| + parser.description = self.module.__doc__.strip() + '\n' |
| + else: |
| + parser.description = command.__doc__.strip() or '' |
| + if parser.description: |
| + parser.description += '\n' |
| + parser.set_usage('usage: %%prog %s [options] %s' % (name, more)) |
| + |
| + def execute(self, parser, args): |
| + """Dispatches execution to the right command. |
| + |
| + Fallbacks to 'help'. |
| + """ |
| + commands = self.get_commands() |
| + length = max(len(c) for c in commands) |
| + |
| + def gen_summary(x): |
| + """Creates a oneline summary from the docstring.""" |
| + line = x.__doc__.split('\n', 1)[0].rstrip('.') |
| + return (line[0].lower() + line[1:]).strip() |
| + |
| + docs = sorted( |
| + (name, gen_summary(handler)) for name, handler in commands.iteritems()) |
| + |
| + # Lists all the commands in 'help'. |
| + if commands['help']: |
| + commands['help'].usage_more = ( |
| + '\n\nCommands are:\n' + '\n'.join( |
| + ' %-*s %s' % (length, name, doc) for name, doc in docs)) |
| + |
| + 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.get_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.get_command(args[0]) |
| + |
| + # "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 |