Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | |
| 2 # Use of this source code is governed by a BSD-style license that can be | |
| 3 # found in the LICENSE file. | |
| 4 | |
| 5 """Manages subcommands in a script. | |
| 6 | |
| 7 Each subcommand should look like this: | |
| 8 @usage('[pet name]') | |
| 9 def CMDpet(parser, args): | |
| 10 '''Prints a pet. | |
| 11 | |
| 12 Many people likes pet. This command prints a pet for your pleasure. | |
| 13 ''' | |
| 14 parser.add_option('--color', help='color of your pet') | |
| 15 options, args = parser.parse_args(args) | |
| 16 if len(args) != 1: | |
| 17 parser.error('A pet name is required') | |
| 18 pet = args[0] | |
| 19 if options.color: | |
| 20 print('Nice %s %d' % (options.color, pet)) | |
| 21 else: | |
| 22 print('Nice %s' % pet) | |
| 23 return 0 | |
| 24 | |
| 25 Explanation: | |
| 26 - usage decorator alter the usage: line in the command's help. | |
| 27 - docstring is used to both short help line and long help line. | |
| 28 - parser can be augmented with arguments. | |
| 29 - return the exit code. | |
| 30 - Every function in the main module with a name starting with 'CMD' will be a | |
| 31 subcommand. | |
| 32 - The module's docstring will be used in the default help page. | |
| 33 """ | |
| 34 | |
| 35 import difflib | |
| 36 import sys | |
| 37 | |
| 38 # This is the main module where to search for commands. It should never be | |
| 39 # modified except for unit tests, where the main module is the unit test so this | |
| 40 # value needs to be updated manually. | |
| 41 MAIN_MODULE = sys.modules['__main__'] | |
|
iannucci
2013/08/15 20:46:04
Hm.. I don't like this (esp. modifying it in the t
M-A Ruel
2013/08/15 20:56:55
I'll prototype this and will reply.
| |
| 42 | |
| 43 | |
| 44 def usage(more): | |
| 45 """Adds a 'usage_more' property to a CMD function.""" | |
| 46 def hook(fn): | |
| 47 fn.usage_more = more | |
| 48 return fn | |
| 49 return hook | |
|
iannucci
2013/08/15 20:46:04
I've actually always wondered why this doesn't jus
M-A Ruel
2013/08/15 20:56:55
wraps() is not applicable because it's not a hook,
iannucci
2013/08/15 21:23:22
Ah right.
| |
| 50 | |
| 51 | |
| 52 def get_commands(): | |
| 53 """Returns a dict of command and their handling function. | |
| 54 | |
| 55 The commands must be in the '__main__' modules. To import a command from a | |
|
iannucci
2013/08/15 20:46:04
I think some of these docstrings need to be update
| |
| 56 submodule, use: | |
| 57 from mysubcommand import CMDfoo | |
| 58 | |
| 59 Automatically adds 'help' if not already defined. | |
| 60 | |
| 61 A command can be effectively disabled by defining a global variable to None, | |
| 62 e.g.: | |
| 63 CMDhelp = None | |
| 64 """ | |
| 65 cmds = dict( | |
| 66 (fn[3:], getattr(MAIN_MODULE, fn)) | |
| 67 for fn in dir(MAIN_MODULE) if fn.startswith('CMD')) | |
| 68 cmds.setdefault('help', CMDhelp) | |
| 69 return cmds | |
| 70 | |
| 71 | |
| 72 def get_command(name): | |
|
iannucci
2013/08/15 20:46:04
Hm... I would expect this to be equivalent to get_
M-A Ruel
2013/08/15 20:56:55
That was ripped directly from git_cl.py The docstr
iannucci
2013/08/15 21:23:22
maybe find_closest_command?
M-A Ruel
2013/08/16 13:47:20
Done.
| |
| 73 """Retrieves the function to handle a command. | |
| 74 | |
| 75 It automatically tries to guess the intended command by handling typos or | |
| 76 incomplete names. | |
| 77 """ | |
| 78 commands = get_commands() | |
| 79 if name in commands: | |
| 80 return commands[name] | |
| 81 | |
| 82 # An exact match was not found. Try to be smart and look if there's something | |
| 83 # similar. | |
| 84 commands_with_prefix = [c for c in commands if c.startswith(name)] | |
| 85 if len(commands_with_prefix) == 1: | |
| 86 return commands[commands_with_prefix[0]] | |
| 87 | |
| 88 # A #closeenough approximation of levenshtein distance. | |
| 89 def close_enough(a, b): | |
| 90 return difflib.SequenceMatcher(a=a, b=b).ratio() | |
| 91 | |
| 92 hamming_commands = sorted( | |
| 93 ((close_enough(c, name), c) for c in commands), | |
| 94 reverse=True) | |
| 95 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3: | |
| 96 # Too ambiguous. | |
| 97 return | |
| 98 | |
| 99 if hamming_commands[0][0] < 0.8: | |
| 100 # Not similar enough. Don't be a fool and run a random command. | |
| 101 return | |
| 102 | |
| 103 return commands[hamming_commands[0][1]] | |
| 104 | |
| 105 | |
| 106 def CMDhelp(parser, args): | |
| 107 """Prints list of commands or help for a specific command.""" | |
| 108 # This is the default help implementation. It can be disabled or overriden if | |
| 109 # wanted. | |
| 110 if not any(i in ('-h', '--help') for i in args): | |
| 111 args = args + ['--help'] | |
| 112 _, args = parser.parse_args(args) | |
| 113 # Never gets there. | |
| 114 assert False | |
| 115 | |
| 116 | |
| 117 def add_command_usage(parser, command): | |
| 118 """Modifies an OptionParser object with the function's documentation.""" | |
| 119 name = command.__name__[3:] | |
| 120 more = getattr(command, 'usage_more', '') | |
| 121 if name == 'help': | |
| 122 name = '<command>' | |
| 123 # Use the __main__ module docstring as the help. | |
| 124 parser.description = sys.modules['__main__'].__doc__.strip() + '\n' | |
|
iannucci
2013/08/15 20:46:04
This shouldn't be sys.modules['__main__'], I don't
M-A Ruel
2013/08/15 20:56:55
Exact, fixing.
| |
| 125 else: | |
| 126 parser.description = command.__doc__.strip() or '' | |
| 127 if parser.description: | |
| 128 parser.description += '\n' | |
| 129 parser.set_usage('usage: %%prog %s [options] %s' % (name, more)) | |
| 130 | |
| 131 | |
| 132 def dispatch_command(parser, args): | |
| 133 """Dispatches execution to the right command. | |
| 134 | |
| 135 Fallbacks to 'help'. | |
| 136 """ | |
| 137 commands = get_commands() | |
| 138 length = max(len(c) for c in commands) | |
| 139 | |
| 140 def gen_summary(x): | |
| 141 """Creates a oneline summary from the docstring.""" | |
| 142 line = x.__doc__.split('\n', 1)[0].rstrip('.') | |
| 143 return (line[0].lower() + line[1:]).strip() | |
| 144 | |
| 145 docs = sorted( | |
| 146 (name, gen_summary(handler)) for name, handler in commands.iteritems()) | |
| 147 | |
| 148 # Lists all the commands in 'help'. | |
| 149 if commands['help']: | |
| 150 commands['help'].usage_more = ( | |
| 151 '\n\nCommands are:\n' + '\n'.join( | |
| 152 ' %-*s %s' % (length, name, doc) for name, doc in docs)) | |
| 153 | |
| 154 if args: | |
| 155 if args[0] in ('-h', '--help') and len(args) > 1: | |
| 156 # Inverse the argument order so 'tool --help cmd' is rewritten to | |
| 157 # 'tool cmd --help'. | |
| 158 args = [args[1], args[0]] + args[2:] | |
| 159 command = get_command(args[0]) | |
| 160 if command: | |
| 161 if command.__name__ == 'CMDhelp' and len(args) > 1: | |
| 162 # Inverse the arguments order so 'tool help cmd' is rewritten to | |
| 163 # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work | |
| 164 # too. | |
| 165 args = [args[1], '--help'] + args[2:] | |
| 166 command = get_command(args[0]) | |
| 167 | |
| 168 # "fix" the usage and the description now that we know the subcommand. | |
| 169 add_command_usage(parser, command) | |
| 170 return command(parser, args[1:]) | |
| 171 | |
| 172 if commands['help']: | |
| 173 # Not a known command. Default to help. | |
| 174 add_command_usage(parser, commands['help']) | |
| 175 return commands['help'](parser, args) | |
| 176 | |
| 177 # Nothing can be done. | |
| 178 return 2 | |
| OLD | NEW |