OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.test.test_usage -*- | |
2 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """ | |
7 twisted.python.usage is a module for parsing/handling the | |
8 command line of your program. | |
9 | |
10 For information on how to use it, see | |
11 U{http://twistedmatrix.com/projects/core/documentation/howto/options.html}, | |
12 or doc/howto/options.html in your Twisted directory. | |
13 """ | |
14 | |
15 # System Imports | |
16 import os | |
17 import sys | |
18 import getopt | |
19 from os import path | |
20 | |
21 # Sibling Imports | |
22 from twisted.python import reflect, text, util | |
23 | |
24 | |
25 class UsageError(Exception): | |
26 pass | |
27 | |
28 | |
29 error = UsageError | |
30 | |
31 | |
32 class CoerceParameter(object): | |
33 """ | |
34 Utility class that can corce a parameter before storing it. | |
35 """ | |
36 def __init__(self, options, coerce): | |
37 """ | |
38 @param options: parent Options object | |
39 @param coerce: callable used to coerce the value. | |
40 """ | |
41 self.options = options | |
42 self.coerce = coerce | |
43 self.doc = getattr(self.coerce, 'coerceDoc', '') | |
44 | |
45 def dispatch(self, parameterName, value): | |
46 """ | |
47 When called in dispatch, do the coerce for C{value} and save the | |
48 returned value. | |
49 """ | |
50 if value is None: | |
51 raise UsageError("Parameter '%s' requires an argument." | |
52 % (parameterName,)) | |
53 try: | |
54 value = self.coerce(value) | |
55 except ValueError, e: | |
56 raise UsageError("Parameter type enforcement failed: %s" % (e,)) | |
57 | |
58 self.options.opts[parameterName] = value | |
59 | |
60 | |
61 class Options(dict): | |
62 """ | |
63 An option list parser class | |
64 | |
65 C{optFlags} and C{optParameters} are lists of available parameters | |
66 which your program can handle. The difference between the two | |
67 is the 'flags' have an on(1) or off(0) state (off by default) | |
68 whereas 'parameters' have an assigned value, with an optional | |
69 default. (Compare '--verbose' and '--verbosity=2') | |
70 | |
71 optFlags is assigned a list of lists. Each list represents | |
72 a flag parameter, as so:: | |
73 | |
74 | optFlags = [['verbose', 'v', 'Makes it tell you what it doing.'], | |
75 | ['quiet', 'q', 'Be vewy vewy quiet.']] | |
76 | |
77 As you can see, the first item is the long option name | |
78 (prefixed with '--' on the command line), followed by the | |
79 short option name (prefixed with '-'), and the description. | |
80 The description is used for the built-in handling of the | |
81 --help switch, which prints a usage summary. | |
82 | |
83 C{optParameters} is much the same, except the list also contains | |
84 a default value:: | |
85 | |
86 | optParameters = [['outfile', 'O', 'outfile.log', 'Description...']] | |
87 | |
88 A coerce function can also be specified as the last element: it will be | |
89 called with the argument and should return the value that will be stored | |
90 for the option. This function can have a C{coerceDoc} attribute which | |
91 will be appended to the documentation of the option. | |
92 | |
93 subCommands is a list of 4-tuples of (command name, command shortcut, | |
94 parser class, documentation). If the first non-option argument found is | |
95 one of the given command names, an instance of the given parser class is | |
96 instantiated and given the remainder of the arguments to parse and | |
97 self.opts[command] is set to the command name. For example:: | |
98 | |
99 | subCommands = [ | |
100 | ['inquisition', 'inquest', InquisitionOptions, | |
101 | 'Perform an inquisition'], | |
102 | ['holyquest', 'quest', HolyQuestOptions, | |
103 | 'Embark upon a holy quest'] | |
104 | ] | |
105 | |
106 In this case, C{"<program> holyquest --horseback --for-grail"} will cause | |
107 C{HolyQuestOptions} to be instantiated and asked to parse | |
108 C{['--horseback', '--for-grail']}. Currently, only the first sub-command | |
109 is parsed, and all options following it are passed to its parser. If a | |
110 subcommand is found, the subCommand attribute is set to its name and the | |
111 subOptions attribute is set to the Option instance that parses the | |
112 remaining options. If a subcommand is not given to parseOptions, | |
113 the subCommand attribute will be None. You can also mark one of | |
114 the subCommands to be the default. | |
115 | |
116 | defaultSubCommand = 'holyquest' | |
117 | |
118 In this case, the subCommand attribute will never be None, and | |
119 the subOptions attribute will always be set. | |
120 | |
121 If you want to handle your own options, define a method named | |
122 C{opt_paramname} that takes C{(self, option)} as arguments. C{option} | |
123 will be whatever immediately follows the parameter on the | |
124 command line. Options fully supports the mapping interface, so you | |
125 can do things like C{'self["option"] = val'} in these methods. | |
126 | |
127 Advanced functionality is covered in the howto documentation, | |
128 available at | |
129 U{http://twistedmatrix.com/projects/core/documentation/howto/options.html}, | |
130 or doc/howto/options.html in your Twisted directory. | |
131 """ | |
132 | |
133 subCommand = None | |
134 defaultSubCommand = None | |
135 parent = None | |
136 def __init__(self): | |
137 super(Options, self).__init__() | |
138 | |
139 self.opts = self | |
140 self.defaults = {} | |
141 | |
142 # These are strings/lists we will pass to getopt | |
143 self.longOpt = [] | |
144 self.shortOpt = '' | |
145 self.docs = {} | |
146 self.synonyms = {} | |
147 self._dispatch = {} | |
148 | |
149 | |
150 collectors = [ | |
151 self._gather_flags, | |
152 self._gather_parameters, | |
153 self._gather_handlers, | |
154 ] | |
155 | |
156 for c in collectors: | |
157 (longOpt, shortOpt, docs, settings, synonyms, dispatch) = c() | |
158 self.longOpt.extend(longOpt) | |
159 self.shortOpt = self.shortOpt + shortOpt | |
160 self.docs.update(docs) | |
161 | |
162 self.opts.update(settings) | |
163 self.defaults.update(settings) | |
164 | |
165 self.synonyms.update(synonyms) | |
166 self._dispatch.update(dispatch) | |
167 | |
168 def __hash__(self): | |
169 """ | |
170 Define a custom hash function so that Options instances can be used | |
171 as dictionary keys. This is an internal feature used to implement | |
172 the parser. Do not rely on it in application code. | |
173 """ | |
174 return int(id(self) % sys.maxint) | |
175 | |
176 def opt_help(self): | |
177 """ | |
178 Display this help and exit. | |
179 """ | |
180 print self.__str__() | |
181 sys.exit(0) | |
182 | |
183 def opt_version(self): | |
184 from twisted import copyright | |
185 print "Twisted version:", copyright.version | |
186 sys.exit(0) | |
187 | |
188 #opt_h = opt_help # this conflicted with existing 'host' options. | |
189 | |
190 def parseOptions(self, options=None): | |
191 """ | |
192 The guts of the command-line parser. | |
193 """ | |
194 | |
195 if options is None: | |
196 options = sys.argv[1:] | |
197 try: | |
198 opts, args = getopt.getopt(options, | |
199 self.shortOpt, self.longOpt) | |
200 except getopt.error, e: | |
201 raise UsageError(str(e)) | |
202 | |
203 for opt, arg in opts: | |
204 if opt[1] == '-': | |
205 opt = opt[2:] | |
206 else: | |
207 opt = opt[1:] | |
208 | |
209 optMangled = opt | |
210 if optMangled not in self.synonyms: | |
211 optMangled = opt.replace("-", "_") | |
212 if optMangled not in self.synonyms: | |
213 raise UsageError("No such option '%s'" % (opt,)) | |
214 | |
215 optMangled = self.synonyms[optMangled] | |
216 if isinstance(self._dispatch[optMangled], CoerceParameter): | |
217 self._dispatch[optMangled].dispatch(optMangled, arg) | |
218 else: | |
219 self._dispatch[optMangled](optMangled, arg) | |
220 | |
221 if (getattr(self, 'subCommands', None) | |
222 and (args or self.defaultSubCommand is not None)): | |
223 if not args: | |
224 args = [self.defaultSubCommand] | |
225 sub, rest = args[0], args[1:] | |
226 for (cmd, short, parser, doc) in self.subCommands: | |
227 if sub == cmd or sub == short: | |
228 self.subCommand = cmd | |
229 self.subOptions = parser() | |
230 self.subOptions.parent = self | |
231 self.subOptions.parseOptions(rest) | |
232 break | |
233 else: | |
234 raise UsageError("Unknown command: %s" % sub) | |
235 else: | |
236 try: | |
237 self.parseArgs(*args) | |
238 except TypeError: | |
239 raise UsageError("Wrong number of arguments.") | |
240 | |
241 self.postOptions() | |
242 | |
243 def postOptions(self): | |
244 """ | |
245 I am called after the options are parsed. | |
246 | |
247 Override this method in your subclass to do something after | |
248 the options have been parsed and assigned, like validate that | |
249 all options are sane. | |
250 """ | |
251 | |
252 def parseArgs(self): | |
253 """ | |
254 I am called with any leftover arguments which were not options. | |
255 | |
256 Override me to do something with the remaining arguments on | |
257 the command line, those which were not flags or options. e.g. | |
258 interpret them as a list of files to operate on. | |
259 | |
260 Note that if there more arguments on the command line | |
261 than this method accepts, parseArgs will blow up with | |
262 a getopt.error. This means if you don't override me, | |
263 parseArgs will blow up if I am passed any arguments at | |
264 all! | |
265 """ | |
266 | |
267 def _generic_flag(self, flagName, value=None): | |
268 if value not in ('', None): | |
269 raise UsageError("Flag '%s' takes no argument." | |
270 " Not even \"%s\"." % (flagName, value)) | |
271 | |
272 self.opts[flagName] = 1 | |
273 | |
274 def _gather_flags(self): | |
275 """ | |
276 Gather up boolean (flag) options. | |
277 """ | |
278 | |
279 longOpt, shortOpt = [], '' | |
280 docs, settings, synonyms, dispatch = {}, {}, {}, {} | |
281 | |
282 flags = [] | |
283 reflect.accumulateClassList(self.__class__, 'optFlags', flags) | |
284 | |
285 for flag in flags: | |
286 long, short, doc = util.padTo(3, flag) | |
287 if not long: | |
288 raise ValueError("A flag cannot be without a name.") | |
289 | |
290 docs[long] = doc | |
291 settings[long] = 0 | |
292 if short: | |
293 shortOpt = shortOpt + short | |
294 synonyms[short] = long | |
295 longOpt.append(long) | |
296 synonyms[long] = long | |
297 dispatch[long] = self._generic_flag | |
298 | |
299 return longOpt, shortOpt, docs, settings, synonyms, dispatch | |
300 | |
301 def _gather_parameters(self): | |
302 """ | |
303 Gather options which take a value. | |
304 """ | |
305 longOpt, shortOpt = [], '' | |
306 docs, settings, synonyms, dispatch = {}, {}, {}, {} | |
307 | |
308 parameters = [] | |
309 | |
310 reflect.accumulateClassList(self.__class__, 'optStrings', | |
311 parameters) | |
312 if parameters: | |
313 import warnings | |
314 warnings.warn("Options.optStrings is deprecated, " | |
315 "please use optParameters instead.", stacklevel=2) | |
316 | |
317 reflect.accumulateClassList(self.__class__, 'optParameters', | |
318 parameters) | |
319 | |
320 synonyms = {} | |
321 | |
322 for parameter in parameters: | |
323 long, short, default, doc, paramType = util.padTo(5, parameter) | |
324 if not long: | |
325 raise ValueError("A parameter cannot be without a name.") | |
326 | |
327 docs[long] = doc | |
328 settings[long] = default | |
329 if short: | |
330 shortOpt = shortOpt + short + ':' | |
331 synonyms[short] = long | |
332 longOpt.append(long + '=') | |
333 synonyms[long] = long | |
334 if paramType is not None: | |
335 dispatch[long] = CoerceParameter(self, paramType) | |
336 else: | |
337 dispatch[long] = CoerceParameter(self, str) | |
338 | |
339 return longOpt, shortOpt, docs, settings, synonyms, dispatch | |
340 | |
341 | |
342 def _gather_handlers(self): | |
343 """ | |
344 Gather up options with their own handler methods. | |
345 """ | |
346 | |
347 longOpt, shortOpt = [], '' | |
348 docs, settings, synonyms, dispatch = {}, {}, {}, {} | |
349 | |
350 dct = {} | |
351 reflect.addMethodNamesToDict(self.__class__, dct, "opt_") | |
352 | |
353 for name in dct.keys(): | |
354 method = getattr(self, 'opt_'+name) | |
355 | |
356 takesArg = not flagFunction(method, name) | |
357 | |
358 prettyName = name.replace('_', '-') | |
359 doc = getattr(method, '__doc__', None) | |
360 if doc: | |
361 ## Only use the first line. | |
362 #docs[name] = doc.split('\n')[0] | |
363 docs[prettyName] = doc | |
364 else: | |
365 docs[prettyName] = self.docs.get(prettyName) | |
366 | |
367 synonyms[prettyName] = prettyName | |
368 | |
369 # A little slight-of-hand here makes dispatching much easier | |
370 # in parseOptions, as it makes all option-methods have the | |
371 # same signature. | |
372 if takesArg: | |
373 fn = lambda name, value, m=method: m(value) | |
374 else: | |
375 # XXX: This won't raise a TypeError if it's called | |
376 # with a value when it shouldn't be. | |
377 fn = lambda name, value=None, m=method: m() | |
378 | |
379 dispatch[prettyName] = fn | |
380 | |
381 if len(name) == 1: | |
382 shortOpt = shortOpt + name | |
383 if takesArg: | |
384 shortOpt = shortOpt + ':' | |
385 else: | |
386 if takesArg: | |
387 prettyName = prettyName + '=' | |
388 longOpt.append(prettyName) | |
389 | |
390 reverse_dct = {} | |
391 # Map synonyms | |
392 for name in dct.keys(): | |
393 method = getattr(self, 'opt_' + name) | |
394 if method not in reverse_dct: | |
395 reverse_dct[method] = [] | |
396 reverse_dct[method].append(name) | |
397 | |
398 cmpLength = lambda a, b: cmp(len(a), len(b)) | |
399 | |
400 for method, names in reverse_dct.items(): | |
401 if len(names) < 2: | |
402 continue | |
403 names_ = names[:] | |
404 names_.sort(cmpLength) | |
405 longest = names_.pop() | |
406 for name in names_: | |
407 synonyms[name] = longest | |
408 | |
409 return longOpt, shortOpt, docs, settings, synonyms, dispatch | |
410 | |
411 | |
412 def __str__(self): | |
413 return self.getSynopsis() + '\n' + self.getUsage(width=None) | |
414 | |
415 def getSynopsis(self): | |
416 """ | |
417 Returns a string containing a description of these options and how to | |
418 pass them to the executed file. | |
419 """ | |
420 | |
421 default = "%s%s" % (path.basename(sys.argv[0]), | |
422 (self.longOpt and " [options]") or '') | |
423 if self.parent is None: | |
424 default = "Usage: %s%s" % (path.basename(sys.argv[0]), | |
425 (self.longOpt and " [options]") or '') | |
426 else: | |
427 default = '%s' % ((self.longOpt and "[options]") or '') | |
428 synopsis = getattr(self, "synopsis", default) | |
429 | |
430 synopsis = synopsis.rstrip() | |
431 | |
432 if self.parent is not None: | |
433 synopsis = ' '.join((self.parent.getSynopsis(), | |
434 self.parent.subCommand, synopsis)) | |
435 | |
436 return synopsis | |
437 | |
438 def getUsage(self, width=None): | |
439 # If subOptions exists by now, then there was probably an error while | |
440 # parsing its options. | |
441 if hasattr(self, 'subOptions'): | |
442 return self.subOptions.getUsage(width=width) | |
443 | |
444 if not width: | |
445 width = int(os.environ.get('COLUMNS', '80')) | |
446 | |
447 if hasattr(self, 'subCommands'): | |
448 cmdDicts = [] | |
449 for (cmd, short, parser, desc) in self.subCommands: | |
450 cmdDicts.append( | |
451 {'long': cmd, | |
452 'short': short, | |
453 'doc': desc, | |
454 'optType': 'command', | |
455 'default': None | |
456 }) | |
457 chunks = docMakeChunks(cmdDicts, width) | |
458 commands = 'Commands:\n' + ''.join(chunks) | |
459 else: | |
460 commands = '' | |
461 | |
462 longToShort = {} | |
463 for key, value in self.synonyms.items(): | |
464 longname = value | |
465 if (key != longname) and (len(key) == 1): | |
466 longToShort[longname] = key | |
467 else: | |
468 if longname not in longToShort: | |
469 longToShort[longname] = None | |
470 else: | |
471 pass | |
472 | |
473 optDicts = [] | |
474 for opt in self.longOpt: | |
475 if opt[-1] == '=': | |
476 optType = 'parameter' | |
477 opt = opt[:-1] | |
478 else: | |
479 optType = 'flag' | |
480 | |
481 optDicts.append( | |
482 {'long': opt, | |
483 'short': longToShort[opt], | |
484 'doc': self.docs[opt], | |
485 'optType': optType, | |
486 'default': self.defaults.get(opt, None), | |
487 'dispatch': self._dispatch.get(opt, None) | |
488 }) | |
489 | |
490 if not (getattr(self, "longdesc", None) is None): | |
491 longdesc = self.longdesc | |
492 else: | |
493 import __main__ | |
494 if getattr(__main__, '__doc__', None): | |
495 longdesc = __main__.__doc__ | |
496 else: | |
497 longdesc = '' | |
498 | |
499 if longdesc: | |
500 longdesc = ('\n' + | |
501 '\n'.join(text.wordWrap(longdesc, width)).strip() | |
502 + '\n') | |
503 | |
504 if optDicts: | |
505 chunks = docMakeChunks(optDicts, width) | |
506 s = "Options:\n%s" % (''.join(chunks)) | |
507 else: | |
508 s = "Options: None\n" | |
509 | |
510 return s + longdesc + commands | |
511 | |
512 #def __repr__(self): | |
513 # XXX: It'd be cool if we could return a succinct representation | |
514 # of which flags and options are set here. | |
515 | |
516 | |
517 def docMakeChunks(optList, width=80): | |
518 """ | |
519 Makes doc chunks for option declarations. | |
520 | |
521 Takes a list of dictionaries, each of which may have one or more | |
522 of the keys 'long', 'short', 'doc', 'default', 'optType'. | |
523 | |
524 Returns a list of strings. | |
525 The strings may be multiple lines, | |
526 all of them end with a newline. | |
527 """ | |
528 | |
529 # XXX: sanity check to make sure we have a sane combination of keys. | |
530 | |
531 maxOptLen = 0 | |
532 for opt in optList: | |
533 optLen = len(opt.get('long', '')) | |
534 if optLen: | |
535 if opt.get('optType', None) == "parameter": | |
536 # these take up an extra character | |
537 optLen = optLen + 1 | |
538 maxOptLen = max(optLen, maxOptLen) | |
539 | |
540 colWidth1 = maxOptLen + len(" -s, -- ") | |
541 colWidth2 = width - colWidth1 | |
542 # XXX - impose some sane minimum limit. | |
543 # Then if we don't have enough room for the option and the doc | |
544 # to share one line, they can take turns on alternating lines. | |
545 | |
546 colFiller1 = " " * colWidth1 | |
547 | |
548 optChunks = [] | |
549 seen = {} | |
550 for opt in optList: | |
551 if opt.get('short', None) in seen or opt.get('long', None) in seen: | |
552 continue | |
553 for x in opt.get('short', None), opt.get('long', None): | |
554 if x is not None: | |
555 seen[x] = 1 | |
556 | |
557 optLines = [] | |
558 comma = " " | |
559 if opt.get('short', None): | |
560 short = "-%c" % (opt['short'],) | |
561 else: | |
562 short = '' | |
563 | |
564 if opt.get('long', None): | |
565 long = opt['long'] | |
566 if opt.get("optType", None) == "parameter": | |
567 long = long + '=' | |
568 | |
569 long = "%-*s" % (maxOptLen, long) | |
570 if short: | |
571 comma = "," | |
572 else: | |
573 long = " " * (maxOptLen + len('--')) | |
574 | |
575 if opt.get('optType', None) == 'command': | |
576 column1 = ' %s ' % long | |
577 else: | |
578 column1 = " %2s%c --%s " % (short, comma, long) | |
579 | |
580 if opt.get('doc', ''): | |
581 doc = opt['doc'].strip() | |
582 else: | |
583 doc = '' | |
584 | |
585 if (opt.get("optType", None) == "parameter") \ | |
586 and not (opt.get('default', None) is None): | |
587 doc = "%s [default: %s]" % (doc, opt['default']) | |
588 | |
589 if (opt.get("optType", None) == "parameter") \ | |
590 and opt.get('dispatch', None) is not None: | |
591 d = opt['dispatch'] | |
592 if isinstance(d, CoerceParameter) and d.doc: | |
593 doc = "%s. %s" % (doc, d.doc) | |
594 | |
595 if doc: | |
596 column2_l = text.wordWrap(doc, colWidth2) | |
597 else: | |
598 column2_l = [''] | |
599 | |
600 optLines.append("%s%s\n" % (column1, column2_l.pop(0))) | |
601 | |
602 for line in column2_l: | |
603 optLines.append("%s%s\n" % (colFiller1, line)) | |
604 | |
605 optChunks.append(''.join(optLines)) | |
606 | |
607 return optChunks | |
608 | |
609 | |
610 def flagFunction(method, name=None): | |
611 reqArgs = method.im_func.func_code.co_argcount | |
612 if reqArgs > 2: | |
613 raise UsageError('Invalid Option function for %s' % | |
614 (name or method.func_name)) | |
615 if reqArgs == 2: | |
616 # argName = method.im_func.func_code.co_varnames[1] | |
617 return 0 | |
618 return 1 | |
619 | |
620 | |
621 def portCoerce(value): | |
622 """ | |
623 Coerce a string value to an int port number, and checks the validity. | |
624 """ | |
625 value = int(value) | |
626 if value < 0 or value > 65535: | |
627 raise ValueError("Port number not in range: %s" % (value,)) | |
628 return value | |
629 portCoerce.coerceDoc = "Must be an int between 0 and 65535." | |
630 | |
631 | |
OLD | NEW |