OLD | NEW |
1 # Copyright 2013 The Chromium Authors. All rights reserved. | 1 # Copyright 2013 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Manages subcommands in a script. | 5 """Manages subcommands in a script. |
6 | 6 |
7 Each subcommand should look like this: | 7 Each subcommand should look like this: |
8 @usage('[pet name]') | 8 @usage('[pet name]') |
9 def CMDpet(parser, args): | 9 def CMDpet(parser, args): |
10 '''Prints a pet. | 10 '''Prints a pet. |
(...skipping 23 matching lines...) Expand all Loading... |
34 Useful to keep compatibility commands around or aliases. | 34 Useful to keep compatibility commands around or aliases. |
35 - If a command is an alias to another one, it won't be documented. E.g.: | 35 - If a command is an alias to another one, it won't be documented. E.g.: |
36 CMDoldname = CMDnewcmd | 36 CMDoldname = CMDnewcmd |
37 will result in oldname not being documented but supported and redirecting to | 37 will result in oldname not being documented but supported and redirecting to |
38 newcmd. Make it a real function that calls the old function if you want it | 38 newcmd. Make it a real function that calls the old function if you want it |
39 to be documented. | 39 to be documented. |
40 """ | 40 """ |
41 | 41 |
42 import difflib | 42 import difflib |
43 import sys | 43 import sys |
| 44 import textwrap |
44 | 45 |
45 | 46 |
46 def usage(more): | 47 def usage(more): |
47 """Adds a 'usage_more' property to a CMD function.""" | 48 """Adds a 'usage_more' property to a CMD function.""" |
48 def hook(fn): | 49 def hook(fn): |
49 fn.usage_more = more | 50 fn.usage_more = more |
50 return fn | 51 return fn |
51 return hook | 52 return hook |
52 | 53 |
53 | 54 |
| 55 def epilog(text): |
| 56 """Adds an 'epilog' property to a CMD function. |
| 57 |
| 58 It will be shown in the epilog. Usually useful for examples. |
| 59 """ |
| 60 def hook(fn): |
| 61 fn.epilog = text |
| 62 return fn |
| 63 return hook |
| 64 |
| 65 |
54 def CMDhelp(parser, args): | 66 def CMDhelp(parser, args): |
55 """Prints list of commands or help for a specific command.""" | 67 """Prints list of commands or help for a specific command.""" |
56 # This is the default help implementation. It can be disabled or overriden if | 68 # This is the default help implementation. It can be disabled or overriden if |
57 # wanted. | 69 # wanted. |
58 if not any(i in ('-h', '--help') for i in args): | 70 if not any(i in ('-h', '--help') for i in args): |
59 args = args + ['--help'] | 71 args = args + ['--help'] |
60 _, args = parser.parse_args(args) | 72 _, args = parser.parse_args(args) |
61 # Never gets there. | 73 # Never gets there. |
62 assert False | 74 assert False |
63 | 75 |
64 | 76 |
| 77 def _get_color_module(): |
| 78 """Returns the colorama module if available. |
| 79 |
| 80 If so, assumes colors are supported and return the module handle. |
| 81 """ |
| 82 return sys.modules.get('colorama') or sys.modules.get('third_party.colorama') |
| 83 |
| 84 |
65 class CommandDispatcher(object): | 85 class CommandDispatcher(object): |
66 def __init__(self, module): | 86 def __init__(self, module): |
67 """module is the name of the main python module where to look for commands. | 87 """module is the name of the main python module where to look for commands. |
68 | 88 |
69 The python builtin variable __name__ MUST be used for |module|. If the | 89 The python builtin variable __name__ MUST be used for |module|. If the |
70 script is executed in the form 'python script.py', __name__ == '__main__' | 90 script is executed in the form 'python script.py', __name__ == '__main__' |
71 and sys.modules['script'] doesn't exist. On the other hand if it is unit | 91 and sys.modules['script'] doesn't exist. On the other hand if it is unit |
72 tested, __main__ will be the unit test's module so it has to reference to | 92 tested, __main__ will be the unit test's module so it has to reference to |
73 itself with 'script'. __name__ always match the right value. | 93 itself with 'script'. __name__ always match the right value. |
74 """ | 94 """ |
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
119 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3: | 139 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3: |
120 # Too ambiguous. | 140 # Too ambiguous. |
121 return | 141 return |
122 | 142 |
123 if hamming_commands[0][0] < 0.8: | 143 if hamming_commands[0][0] < 0.8: |
124 # Not similar enough. Don't be a fool and run a random command. | 144 # Not similar enough. Don't be a fool and run a random command. |
125 return | 145 return |
126 | 146 |
127 return commands[hamming_commands[0][1]] | 147 return commands[hamming_commands[0][1]] |
128 | 148 |
| 149 def _gen_commands_list(self): |
| 150 """Generates the short list of supported commands.""" |
| 151 commands = self.enumerate_commands() |
| 152 docs = sorted( |
| 153 (name, self._create_command_summary(name, handler)) |
| 154 for name, handler in commands.iteritems()) |
| 155 # Skip commands without a docstring. |
| 156 docs = [i for i in docs if i[1]] |
| 157 # Then calculate maximum length for alignment: |
| 158 length = max(len(c) for c in commands) |
| 159 |
| 160 # Look if color is supported. |
| 161 colors = _get_color_module() |
| 162 green = reset = '' |
| 163 if colors: |
| 164 green = colors.Fore.GREEN |
| 165 reset = colors.Fore.RESET |
| 166 return ( |
| 167 'Commands are:\n' + |
| 168 ''.join( |
| 169 ' %s%-*s%s %s\n' % (green, length, name, reset, doc) |
| 170 for name, doc in docs)) |
| 171 |
129 def _add_command_usage(self, parser, command): | 172 def _add_command_usage(self, parser, command): |
130 """Modifies an OptionParser object with the function's documentation.""" | 173 """Modifies an OptionParser object with the function's documentation.""" |
131 name = command.__name__[3:] | 174 name = command.__name__[3:] |
132 more = getattr(command, 'usage_more', '') | |
133 if name == 'help': | 175 if name == 'help': |
134 name = '<command>' | 176 name = '<command>' |
135 # Use the module's docstring as the description for the 'help' command if | 177 # Use the module's docstring as the description for the 'help' command if |
136 # available. | 178 # available. |
137 parser.description = self.module.__doc__ | 179 parser.description = (self.module.__doc__ or '').rstrip() |
| 180 if parser.description: |
| 181 parser.description += '\n\n' |
| 182 parser.description += self._gen_commands_list() |
| 183 # Do not touch epilog. |
138 else: | 184 else: |
139 # Use the command's docstring if available. | 185 # Use the command's docstring if available. For commands, unlike module |
140 parser.description = command.__doc__ | 186 # docstring, realign. |
141 parser.description = (parser.description or '').strip() | 187 lines = (command.__doc__ or '').rstrip().splitlines() |
142 if parser.description: | 188 if lines[:1]: |
143 parser.description += '\n' | 189 rest = textwrap.dedent('\n'.join(lines[1:])) |
| 190 parser.description = '\n'.join((lines[0], rest)) |
| 191 else: |
| 192 parser.description = lines[0] |
| 193 if parser.description: |
| 194 parser.description += '\n' |
| 195 parser.epilog = getattr(command, 'epilog', None) |
| 196 if parser.epilog: |
| 197 parser.epilog = '\n' + parser.epilog.strip() + '\n' |
| 198 |
| 199 more = getattr(command, 'usage_more', '') |
144 parser.set_usage( | 200 parser.set_usage( |
145 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more)) | 201 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more)) |
146 | 202 |
147 @staticmethod | 203 @staticmethod |
148 def _create_command_summary(name, command): | 204 def _create_command_summary(name, command): |
149 """Creates a oneline summary from the command's docstring.""" | 205 """Creates a oneline summary from the command's docstring.""" |
150 if name != command.__name__[3:]: | 206 if name != command.__name__[3:]: |
151 # Skip aliases. | 207 # Skip aliases. |
152 return '' | 208 return '' |
153 doc = command.__doc__ or '' | 209 doc = command.__doc__ or '' |
154 line = doc.split('\n', 1)[0].rstrip('.') | 210 line = doc.split('\n', 1)[0].rstrip('.') |
155 if not line: | 211 if not line: |
156 return line | 212 return line |
157 return (line[0].lower() + line[1:]).strip() | 213 return (line[0].lower() + line[1:]).strip() |
158 | 214 |
159 def execute(self, parser, args): | 215 def execute(self, parser, args): |
160 """Dispatches execution to the right command. | 216 """Dispatches execution to the right command. |
161 | 217 |
162 Fallbacks to 'help' if not disabled. | 218 Fallbacks to 'help' if not disabled. |
163 """ | 219 """ |
164 commands = self.enumerate_commands() | 220 # Unconditionally disable format_description() and format_epilog(). |
165 length = max(len(c) for c in commands) | 221 # Technically, a formatter should be used but it's not worth (yet) the |
166 | 222 # trouble. |
167 # Lists all the commands in 'help'. | 223 parser.format_description = lambda _: parser.description or '' |
168 if commands['help']: | 224 parser.format_epilog = lambda _: parser.epilog or '' |
169 docs = sorted( | |
170 (name, self._create_command_summary(name, handler)) | |
171 for name, handler in commands.iteritems()) | |
172 # Skip commands without a docstring. | |
173 commands['help'].usage_more = ( | |
174 '\n\nCommands are:\n' + '\n'.join( | |
175 ' %-*s %s' % (length, name, doc) for name, doc in docs if doc)) | |
176 | 225 |
177 if args: | 226 if args: |
178 if args[0] in ('-h', '--help') and len(args) > 1: | 227 if args[0] in ('-h', '--help') and len(args) > 1: |
179 # Inverse the argument order so 'tool --help cmd' is rewritten to | 228 # Inverse the argument order so 'tool --help cmd' is rewritten to |
180 # 'tool cmd --help'. | 229 # 'tool cmd --help'. |
181 args = [args[1], args[0]] + args[2:] | 230 args = [args[1], args[0]] + args[2:] |
182 command = self.find_nearest_command(args[0]) | 231 command = self.find_nearest_command(args[0]) |
183 if command: | 232 if command: |
184 if command.__name__ == 'CMDhelp' and len(args) > 1: | 233 if command.__name__ == 'CMDhelp' and len(args) > 1: |
185 # Inverse the arguments order so 'tool help cmd' is rewritten to | 234 # Inverse the arguments order so 'tool help cmd' is rewritten to |
186 # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work | 235 # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work |
187 # too. | 236 # too. |
188 args = [args[1], '--help'] + args[2:] | 237 args = [args[1], '--help'] + args[2:] |
189 command = self.find_nearest_command(args[0]) or command | 238 command = self.find_nearest_command(args[0]) or command |
190 | 239 |
191 # "fix" the usage and the description now that we know the subcommand. | 240 # "fix" the usage and the description now that we know the subcommand. |
192 self._add_command_usage(parser, command) | 241 self._add_command_usage(parser, command) |
193 return command(parser, args[1:]) | 242 return command(parser, args[1:]) |
194 | 243 |
195 if commands['help']: | 244 cmdhelp = self.enumerate_commands().get('help') |
| 245 if cmdhelp: |
196 # Not a known command. Default to help. | 246 # Not a known command. Default to help. |
197 self._add_command_usage(parser, commands['help']) | 247 self._add_command_usage(parser, cmdhelp) |
198 return commands['help'](parser, args) | 248 return cmdhelp(parser, args) |
199 | 249 |
200 # Nothing can be done. | 250 # Nothing can be done. |
201 return 2 | 251 return 2 |
OLD | NEW |