Index: third_party/depot_tools/subcommand.py |
diff --git a/third_party/depot_tools/subcommand.py b/third_party/depot_tools/subcommand.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..6afe3dcda1d31b458174f97c491dcf4ab0fb68d7 |
--- /dev/null |
+++ b/third_party/depot_tools/subcommand.py |
@@ -0,0 +1,251 @@ |
+# 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 |
+import textwrap |
+ |
+ |
+def usage(more): |
+ """Adds a 'usage_more' property to a CMD function.""" |
+ def hook(fn): |
+ fn.usage_more = more |
+ return fn |
+ return hook |
+ |
+ |
+def epilog(text): |
+ """Adds an 'epilog' property to a CMD function. |
+ |
+ It will be shown in the epilog. Usually useful for examples. |
+ """ |
+ def hook(fn): |
+ fn.epilog = text |
+ 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 |
+ |
+ |
+def _get_color_module(): |
+ """Returns the colorama module if available. |
+ |
+ If so, assumes colors are supported and return the module handle. |
+ """ |
+ return sys.modules.get('colorama') or sys.modules.get('third_party.colorama') |
+ |
+ |
+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 _gen_commands_list(self): |
+ """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()) |
+ # Skip commands without a docstring. |
+ docs = [i for i in docs if i[1]] |
+ # Then calculate maximum length for alignment: |
+ length = max(len(c) for c in commands) |
+ |
+ # Look if color is supported. |
+ colors = _get_color_module() |
+ green = reset = '' |
+ if colors: |
+ green = colors.Fore.GREEN |
+ reset = colors.Fore.RESET |
+ return ( |
+ 'Commands are:\n' + |
+ ''.join( |
+ ' %s%-*s%s %s\n' % (green, length, name, reset, doc) |
+ for 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>' |
+ # Use the module's docstring as the description for the 'help' command if |
+ # available. |
+ parser.description = (self.module.__doc__ or '').rstrip() |
+ if parser.description: |
+ parser.description += '\n\n' |
+ parser.description += self._gen_commands_list() |
+ # Do not touch epilog. |
+ else: |
+ # Use the command's docstring if available. For commands, unlike module |
+ # docstring, realign. |
+ lines = (command.__doc__ or '').rstrip().splitlines() |
+ if lines[:1]: |
+ rest = textwrap.dedent('\n'.join(lines[1:])) |
+ parser.description = '\n'.join((lines[0], rest)) |
+ else: |
+ parser.description = lines[0] |
+ if parser.description: |
+ parser.description += '\n' |
+ parser.epilog = getattr(command, 'epilog', None) |
+ if parser.epilog: |
+ 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)) |
+ |
+ @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. |
+ """ |
+ # Unconditionally disable format_description() and format_epilog(). |
+ # Technically, a formatter should be used but it's not worth (yet) the |
+ # trouble. |
+ parser.format_description = lambda _: parser.description or '' |
+ parser.format_epilog = lambda _: parser.epilog or '' |
+ |
+ 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:]) |
+ |
+ cmdhelp = self.enumerate_commands().get('help') |
+ if cmdhelp: |
+ # Not a known command. Default to help. |
+ self._add_command_usage(parser, cmdhelp) |
+ return cmdhelp(parser, args) |
+ |
+ # Nothing can be done. |
+ return 2 |