1 """distutils.fancy_getopt 2 3 Wrapper around the standard getopt module that provides the following 4 additional features: 5 * short and long options are tied together 6 * options have help strings, so fancy_getopt could potentially 7 create a complete usage summary 8 * options set attributes of a passed-in object 9 """ 10 11 __revision__ = "$Id$" 12 13 import sys 14 import string 15 import re 16 import getopt 17 from distutils.errors import DistutilsGetoptError, DistutilsArgError 18 19 # Much like command_re in distutils.core, this is close to but not quite 20 # the same as a Python NAME -- except, in the spirit of most GNU 21 # utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) 22 # The similarities to NAME are again not a coincidence... 23 longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)' 24 longopt_re = re.compile(r'^%s$' % longopt_pat) 25 26 # For recognizing "negative alias" options, eg. "quiet=!verbose" 27 neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) 28 29 # This is used to translate long options to legitimate Python identifiers 30 # (for use as attributes of some object). 31 longopt_xlate = string.maketrans('-', '_') 32 33 class FancyGetopt: 34 """Wrapper around the standard 'getopt()' module that provides some 35 handy extra functionality: 36 * short and long options are tied together 37 * options have help strings, and help text can be assembled 38 from them 39 * options set attributes of a passed-in object 40 * boolean options can have "negative aliases" -- eg. if 41 --quiet is the "negative alias" of --verbose, then "--quiet" 42 on the command line sets 'verbose' to false 43 """ 44 45 def __init__ (self, option_table=None): 46 47 # The option table is (currently) a list of tuples. The 48 # tuples may have 3 or four values: 49 # (long_option, short_option, help_string [, repeatable]) 50 # if an option takes an argument, its long_option should have '=' 51 # appended; short_option should just be a single character, no ':' 52 # in any case. If a long_option doesn't have a corresponding 53 # short_option, short_option should be None. All option tuples 54 # must have long options. 55 self.option_table = option_table 56 57 # 'option_index' maps long option names to entries in the option 58 # table (ie. those 3-tuples). 59 self.option_index = {} 60 if self.option_table: 61 self._build_index() 62 63 # 'alias' records (duh) alias options; {'foo': 'bar'} means 64 # --foo is an alias for --bar 65 self.alias = {} 66 67 # 'negative_alias' keeps track of options that are the boolean 68 # opposite of some other option 69 self.negative_alias = {} 70 71 # These keep track of the information in the option table. We 72 # don't actually populate these structures until we're ready to 73 # parse the command-line, since the 'option_table' passed in here 74 # isn't necessarily the final word. 75 self.short_opts = [] 76 self.long_opts = [] 77 self.short2long = {} 78 self.attr_name = {} 79 self.takes_arg = {} 80 81 # And 'option_order' is filled up in 'getopt()'; it records the 82 # original order of options (and their values) on the command-line, 83 # but expands short options, converts aliases, etc. 84 self.option_order = [] 85 86 # __init__ () 87 88 89 def _build_index (self): 90 self.option_index.clear() 91 for option in self.option_table: 92 self.option_index[option[0]] = option 93 94 def set_option_table (self, option_table): 95 self.option_table = option_table 96 self._build_index() 97 98 def add_option (self, long_option, short_option=None, help_string=None): 99 if long_option in self.option_index: 100 raise DistutilsGetoptError, \ 101 "option conflict: already an option '%s'" % long_option 102 else: 103 option = (long_option, short_option, help_string) 104 self.option_table.append(option) 105 self.option_index[long_option] = option 106 107 108 def has_option (self, long_option): 109 """Return true if the option table for this parser has an 110 option with long name 'long_option'.""" 111 return long_option in self.option_index 112 113 def get_attr_name (self, long_option): 114 """Translate long option name 'long_option' to the form it 115 has as an attribute of some object: ie., translate hyphens 116 to underscores.""" 117 return string.translate(long_option, longopt_xlate) 118 119 120 def _check_alias_dict (self, aliases, what): 121 assert isinstance(aliases, dict) 122 for (alias, opt) in aliases.items(): 123 if alias not in self.option_index: 124 raise DistutilsGetoptError, \ 125 ("invalid %s '%s': " 126 "option '%s' not defined") % (what, alias, alias) 127 if opt not in self.option_index: 128 raise DistutilsGetoptError, \ 129 ("invalid %s '%s': " 130 "aliased option '%s' not defined") % (what, alias, opt) 131 132 def set_aliases (self, alias): 133 """Set the aliases for this option parser.""" 134 self._check_alias_dict(alias, "alias") 135 self.alias = alias 136 137 def set_negative_aliases (self, negative_alias): 138 """Set the negative aliases for this option parser. 139 'negative_alias' should be a dictionary mapping option names to 140 option names, both the key and value must already be defined 141 in the option table.""" 142 self._check_alias_dict(negative_alias, "negative alias") 143 self.negative_alias = negative_alias 144 145 146 def _grok_option_table (self): 147 """Populate the various data structures that keep tabs on the 148 option table. Called by 'getopt()' before it can do anything 149 worthwhile. 150 """ 151 self.long_opts = [] 152 self.short_opts = [] 153 self.short2long.clear() 154 self.repeat = {} 155 156 for option in self.option_table: 157 if len(option) == 3: 158 long, short, help = option 159 repeat = 0 160 elif len(option) == 4: 161 long, short, help, repeat = option 162 else: 163 # the option table is part of the code, so simply 164 # assert that it is correct 165 raise ValueError, "invalid option tuple: %r" % (option,) 166 167 # Type- and value-check the option names 168 if not isinstance(long, str) or len(long) < 2: 169 raise DistutilsGetoptError, \ 170 ("invalid long option '%s': " 171 "must be a string of length >= 2") % long 172 173 if (not ((short is None) or 174 (isinstance(short, str) and len(short) == 1))): 175 raise DistutilsGetoptError, \ 176 ("invalid short option '%s': " 177 "must a single character or None") % short 178 179 self.repeat[long] = repeat 180 self.long_opts.append(long) 181 182 if long[-1] == '=': # option takes an argument? 183 if short: short = short + ':' 184 long = long[0:-1] 185 self.takes_arg[long] = 1 186 else: 187 188 # Is option is a "negative alias" for some other option (eg. 189 # "quiet" == "!verbose")? 190 alias_to = self.negative_alias.get(long) 191 if alias_to is not None: 192 if self.takes_arg[alias_to]: 193 raise DistutilsGetoptError, \ 194 ("invalid negative alias '%s': " 195 "aliased option '%s' takes a value") % \ 196 (long, alias_to) 197 198 self.long_opts[-1] = long # XXX redundant?! 199 self.takes_arg[long] = 0 200 201 else: 202 self.takes_arg[long] = 0 203 204 # If this is an alias option, make sure its "takes arg" flag is 205 # the same as the option it's aliased to. 206 alias_to = self.alias.get(long) 207 if alias_to is not None: 208 if self.takes_arg[long] != self.takes_arg[alias_to]: 209 raise DistutilsGetoptError, \ 210 ("invalid alias '%s': inconsistent with " 211 "aliased option '%s' (one of them takes a value, " 212 "the other doesn't") % (long, alias_to) 213 214 215 # Now enforce some bondage on the long option name, so we can 216 # later translate it to an attribute name on some object. Have 217 # to do this a bit late to make sure we've removed any trailing 218 # '='. 219 if not longopt_re.match(long): 220 raise DistutilsGetoptError, \ 221 ("invalid long option name '%s' " + 222 "(must be letters, numbers, hyphens only") % long 223 224 self.attr_name[long] = self.get_attr_name(long) 225 if short: 226 self.short_opts.append(short) 227 self.short2long[short[0]] = long 228 229 # for option_table 230 231 # _grok_option_table() 232 233 234 def getopt (self, args=None, object=None): 235 """Parse command-line options in args. Store as attributes on object. 236 237 If 'args' is None or not supplied, uses 'sys.argv[1:]'. If 238 'object' is None or not supplied, creates a new OptionDummy 239 object, stores option values there, and returns a tuple (args, 240 object). If 'object' is supplied, it is modified in place and 241 'getopt()' just returns 'args'; in both cases, the returned 242 'args' is a modified copy of the passed-in 'args' list, which 243 is left untouched. 244 """ 245 if args is None: 246 args = sys.argv[1:] 247 if object is None: 248 object = OptionDummy() 249 created_object = 1 250 else: 251 created_object = 0 252 253 self._grok_option_table() 254 255 short_opts = string.join(self.short_opts) 256 try: 257 opts, args = getopt.getopt(args, short_opts, self.long_opts) 258 except getopt.error, msg: 259 raise DistutilsArgError, msg 260 261 for opt, val in opts: 262 if len(opt) == 2 and opt[0] == '-': # it's a short option 263 opt = self.short2long[opt[1]] 264 else: 265 assert len(opt) > 2 and opt[:2] == '--' 266 opt = opt[2:] 267 268 alias = self.alias.get(opt) 269 if alias: 270 opt = alias 271 272 if not self.takes_arg[opt]: # boolean option? 273 assert val == '', "boolean option can't have value" 274 alias = self.negative_alias.get(opt) 275 if alias: 276 opt = alias 277 val = 0 278 else: 279 val = 1 280 281 attr = self.attr_name[opt] 282 # The only repeating option at the moment is 'verbose'. 283 # It has a negative option -q quiet, which should set verbose = 0. 284 if val and self.repeat.get(attr) is not None: 285 val = getattr(object, attr, 0) + 1 286 setattr(object, attr, val) 287 self.option_order.append((opt, val)) 288 289 # for opts 290 if created_object: 291 return args, object 292 else: 293 return args 294 295 # getopt() 296 297 298 def get_option_order (self): 299 """Returns the list of (option, value) tuples processed by the 300 previous run of 'getopt()'. Raises RuntimeError if 301 'getopt()' hasn't been called yet. 302 """ 303 if self.option_order is None: 304 raise RuntimeError, "'getopt()' hasn't been called yet" 305 else: 306 return self.option_order 307 308 309 def generate_help (self, header=None): 310 """Generate help text (a list of strings, one per suggested line of 311 output) from the option table for this FancyGetopt object. 312 """ 313 # Blithely assume the option table is good: probably wouldn't call 314 # 'generate_help()' unless you've already called 'getopt()'. 315 316 # First pass: determine maximum length of long option names 317 max_opt = 0 318 for option in self.option_table: 319 long = option[0] 320 short = option[1] 321 l = len(long) 322 if long[-1] == '=': 323 l = l - 1 324 if short is not None: 325 l = l + 5 # " (-x)" where short == 'x' 326 if l > max_opt: 327 max_opt = l 328 329 opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter 330 331 # Typical help block looks like this: 332 # --foo controls foonabulation 333 # Help block for longest option looks like this: 334 # --flimflam set the flim-flam level 335 # and with wrapped text: 336 # --flimflam set the flim-flam level (must be between 337 # 0 and 100, except on Tuesdays) 338 # Options with short names will have the short name shown (but 339 # it doesn't contribute to max_opt): 340 # --foo (-f) controls foonabulation 341 # If adding the short option would make the left column too wide, 342 # we push the explanation off to the next line 343 # --flimflam (-l) 344 # set the flim-flam level 345 # Important parameters: 346 # - 2 spaces before option block start lines 347 # - 2 dashes for each long option name 348 # - min. 2 spaces between option and explanation (gutter) 349 # - 5 characters (incl. space) for short option name 350 351 # Now generate lines of help text. (If 80 columns were good enough 352 # for Jesus, then 78 columns are good enough for me!) 353 line_width = 78 354 text_width = line_width - opt_width 355 big_indent = ' ' * opt_width 356 if header: 357 lines = [header] 358 else: 359 lines = ['Option summary:'] 360 361 for option in self.option_table: 362 long, short, help = option[:3] 363 text = wrap_text(help, text_width) 364 if long[-1] == '=': 365 long = long[0:-1] 366 367 # Case 1: no short option at all (makes life easy) 368 if short is None: 369 if text: 370 lines.append(" --%-*s %s" % (max_opt, long, text[0])) 371 else: 372 lines.append(" --%-*s " % (max_opt, long)) 373 374 # Case 2: we have a short option, so we have to include it 375 # just after the long option 376 else: 377 opt_names = "%s (-%s)" % (long, short) 378 if text: 379 lines.append(" --%-*s %s" % 380 (max_opt, opt_names, text[0])) 381 else: 382 lines.append(" --%-*s" % opt_names) 383 384 for l in text[1:]: 385 lines.append(big_indent + l) 386 387 # for self.option_table 388 389 return lines 390 391 # generate_help () 392 393 def print_help (self, header=None, file=None): 394 if file is None: 395 file = sys.stdout 396 for line in self.generate_help(header): 397 file.write(line + "\n") 398 399 # class FancyGetopt 400 401 402 def fancy_getopt (options, negative_opt, object, args): 403 parser = FancyGetopt(options) 404 parser.set_negative_aliases(negative_opt) 405 return parser.getopt(args, object) 406 407 408 WS_TRANS = string.maketrans(string.whitespace, ' ' * len(string.whitespace)) 409 410 def wrap_text (text, width): 411 """wrap_text(text : string, width : int) -> [string] 412 413 Split 'text' into multiple lines of no more than 'width' characters 414 each, and return the list of strings that results. 415 """ 416 417 if text is None: 418 return [] 419 if len(text) <= width: 420 return [text] 421 422 text = string.expandtabs(text) 423 text = string.translate(text, WS_TRANS) 424 chunks = re.split(r'( +|-+)', text) 425 chunks = filter(None, chunks) # ' - ' results in empty strings 426 lines = [] 427 428 while chunks: 429 430 cur_line = [] # list of chunks (to-be-joined) 431 cur_len = 0 # length of current line 432 433 while chunks: 434 l = len(chunks[0]) 435 if cur_len + l <= width: # can squeeze (at least) this chunk in 436 cur_line.append(chunks[0]) 437 del chunks[0] 438 cur_len = cur_len + l 439 else: # this line is full 440 # drop last chunk if all space 441 if cur_line and cur_line[-1][0] == ' ': 442 del cur_line[-1] 443 break 444 445 if chunks: # any chunks left to process? 446 447 # if the current line is still empty, then we had a single 448 # chunk that's too big too fit on a line -- so we break 449 # down and break it up at the line width 450 if cur_len == 0: 451 cur_line.append(chunks[0][0:width]) 452 chunks[0] = chunks[0][width:] 453 454 # all-whitespace chunks at the end of a line can be discarded 455 # (and we know from the re.split above that if a chunk has 456 # *any* whitespace, it is *all* whitespace) 457 if chunks[0][0] == ' ': 458 del chunks[0] 459 460 # and store this line in the list-of-all-lines -- as a single 461 # string, of course! 462 lines.append(string.join(cur_line, '')) 463 464 # while chunks 465 466 return lines 467 468 469 def translate_longopt(opt): 470 """Convert a long option name to a valid Python identifier by 471 changing "-" to "_". 472 """ 473 return string.translate(opt, longopt_xlate) 474 475 476 class OptionDummy: 477 """Dummy class just used as a place to hold command-line option 478 values as instance attributes.""" 479 480 def __init__ (self, options=[]): 481 """Create a new OptionDummy instance. The attributes listed in 482 'options' will be initialized to None.""" 483 for opt in options: 484 setattr(self, opt, None) 485