0001 """distutils.fancy_getopt 0002 0003 Wrapper around the standard getopt module that provides the following 0004 additional features: 0005 * short and long options are tied together 0006 * options have help strings, so fancy_getopt could potentially 0007 create a complete usage summary 0008 * options set attributes of a passed-in object 0009 """ 0010 0011 # This module should be kept compatible with Python 1.5.2. 0012 0013 __revision__ = "$Id: fancy_getopt.py,v 1.29 2004/08/02 17:58:51 fdrake Exp $" 0014 0015 import sys, string, re 0016 from types import * 0017 import getopt 0018 from distutils.errors import * 0019 0020 # Much like command_re in distutils.core, this is close to but not quite 0021 # the same as a Python NAME -- except, in the spirit of most GNU 0022 # utilities, we use '-' in place of '_'. (The spirit of LISP lives on!) 0023 # The similarities to NAME are again not a coincidence... 0024 longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)' 0025 longopt_re = re.compile(r'^%s$' % longopt_pat) 0026 0027 # For recognizing "negative alias" options, eg. "quiet=!verbose" 0028 neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat)) 0029 0030 # This is used to translate long options to legitimate Python identifiers 0031 # (for use as attributes of some object). 0032 longopt_xlate = string.maketrans('-', '_') 0033 0034 class FancyGetopt: 0035 """Wrapper around the standard 'getopt()' module that provides some 0036 handy extra functionality: 0037 * short and long options are tied together 0038 * options have help strings, and help text can be assembled 0039 from them 0040 * options set attributes of a passed-in object 0041 * boolean options can have "negative aliases" -- eg. if 0042 --quiet is the "negative alias" of --verbose, then "--quiet" 0043 on the command line sets 'verbose' to false 0044 """ 0045 0046 def __init__ (self, option_table=None): 0047 0048 # The option table is (currently) a list of tuples. The 0049 # tuples may have 3 or four values: 0050 # (long_option, short_option, help_string [, repeatable]) 0051 # if an option takes an argument, its long_option should have '=' 0052 # appended; short_option should just be a single character, no ':' 0053 # in any case. If a long_option doesn't have a corresponding 0054 # short_option, short_option should be None. All option tuples 0055 # must have long options. 0056 self.option_table = option_table 0057 0058 # 'option_index' maps long option names to entries in the option 0059 # table (ie. those 3-tuples). 0060 self.option_index = {} 0061 if self.option_table: 0062 self._build_index() 0063 0064 # 'alias' records (duh) alias options; {'foo': 'bar'} means 0065 # --foo is an alias for --bar 0066 self.alias = {} 0067 0068 # 'negative_alias' keeps track of options that are the boolean 0069 # opposite of some other option 0070 self.negative_alias = {} 0071 0072 # These keep track of the information in the option table. We 0073 # don't actually populate these structures until we're ready to 0074 # parse the command-line, since the 'option_table' passed in here 0075 # isn't necessarily the final word. 0076 self.short_opts = [] 0077 self.long_opts = [] 0078 self.short2long = {} 0079 self.attr_name = {} 0080 self.takes_arg = {} 0081 0082 # And 'option_order' is filled up in 'getopt()'; it records the 0083 # original order of options (and their values) on the command-line, 0084 # but expands short options, converts aliases, etc. 0085 self.option_order = [] 0086 0087 # __init__ () 0088 0089 0090 def _build_index (self): 0091 self.option_index.clear() 0092 for option in self.option_table: 0093 self.option_index[option[0]] = option 0094 0095 def set_option_table (self, option_table): 0096 self.option_table = option_table 0097 self._build_index() 0098 0099 def add_option (self, long_option, short_option=None, help_string=None): 0100 if self.option_index.has_key(long_option): 0101 raise DistutilsGetoptError, \ 0102 "option conflict: already an option '%s'" % long_option 0103 else: 0104 option = (long_option, short_option, help_string) 0105 self.option_table.append(option) 0106 self.option_index[long_option] = option 0107 0108 0109 def has_option (self, long_option): 0110 """Return true if the option table for this parser has an 0111 option with long name 'long_option'.""" 0112 return self.option_index.has_key(long_option) 0113 0114 def get_attr_name (self, long_option): 0115 """Translate long option name 'long_option' to the form it 0116 has as an attribute of some object: ie., translate hyphens 0117 to underscores.""" 0118 return string.translate(long_option, longopt_xlate) 0119 0120 0121 def _check_alias_dict (self, aliases, what): 0122 assert type(aliases) is DictionaryType 0123 for (alias, opt) in aliases.items(): 0124 if not self.option_index.has_key(alias): 0125 raise DistutilsGetoptError, \ 0126 ("invalid %s '%s': " 0127 "option '%s' not defined") % (what, alias, alias) 0128 if not self.option_index.has_key(opt): 0129 raise DistutilsGetoptError, \ 0130 ("invalid %s '%s': " 0131 "aliased option '%s' not defined") % (what, alias, opt) 0132 0133 def set_aliases (self, alias): 0134 """Set the aliases for this option parser.""" 0135 self._check_alias_dict(alias, "alias") 0136 self.alias = alias 0137 0138 def set_negative_aliases (self, negative_alias): 0139 """Set the negative aliases for this option parser. 0140 'negative_alias' should be a dictionary mapping option names to 0141 option names, both the key and value must already be defined 0142 in the option table.""" 0143 self._check_alias_dict(negative_alias, "negative alias") 0144 self.negative_alias = negative_alias 0145 0146 0147 def _grok_option_table (self): 0148 """Populate the various data structures that keep tabs on the 0149 option table. Called by 'getopt()' before it can do anything 0150 worthwhile. 0151 """ 0152 self.long_opts = [] 0153 self.short_opts = [] 0154 self.short2long.clear() 0155 self.repeat = {} 0156 0157 for option in self.option_table: 0158 if len(option) == 3: 0159 long, short, help = option 0160 repeat = 0 0161 elif len(option) == 4: 0162 long, short, help, repeat = option 0163 else: 0164 # the option table is part of the code, so simply 0165 # assert that it is correct 0166 raise ValueError, "invalid option tuple: %r" % (option,) 0167 0168 # Type- and value-check the option names 0169 if type(long) is not StringType or len(long) < 2: 0170 raise DistutilsGetoptError, \ 0171 ("invalid long option '%s': " 0172 "must be a string of length >= 2") % long 0173 0174 if (not ((short is None) or 0175 (type(short) is StringType and len(short) == 1))): 0176 raise DistutilsGetoptError, \ 0177 ("invalid short option '%s': " 0178 "must a single character or None") % short 0179 0180 self.repeat[long] = repeat 0181 self.long_opts.append(long) 0182 0183 if long[-1] == '=': # option takes an argument? 0184 if short: short = short + ':' 0185 long = long[0:-1] 0186 self.takes_arg[long] = 1 0187 else: 0188 0189 # Is option is a "negative alias" for some other option (eg. 0190 # "quiet" == "!verbose")? 0191 alias_to = self.negative_alias.get(long) 0192 if alias_to is not None: 0193 if self.takes_arg[alias_to]: 0194 raise DistutilsGetoptError, \ 0195 ("invalid negative alias '%s': " 0196 "aliased option '%s' takes a value") % \ 0197 (long, alias_to) 0198 0199 self.long_opts[-1] = long # XXX redundant?! 0200 self.takes_arg[long] = 0 0201 0202 else: 0203 self.takes_arg[long] = 0 0204 0205 # If this is an alias option, make sure its "takes arg" flag is 0206 # the same as the option it's aliased to. 0207 alias_to = self.alias.get(long) 0208 if alias_to is not None: 0209 if self.takes_arg[long] != self.takes_arg[alias_to]: 0210 raise DistutilsGetoptError, \ 0211 ("invalid alias '%s': inconsistent with " 0212 "aliased option '%s' (one of them takes a value, " 0213 "the other doesn't") % (long, alias_to) 0214 0215 0216 # Now enforce some bondage on the long option name, so we can 0217 # later translate it to an attribute name on some object. Have 0218 # to do this a bit late to make sure we've removed any trailing 0219 # '='. 0220 if not longopt_re.match(long): 0221 raise DistutilsGetoptError, \ 0222 ("invalid long option name '%s' " + 0223 "(must be letters, numbers, hyphens only") % long 0224 0225 self.attr_name[long] = self.get_attr_name(long) 0226 if short: 0227 self.short_opts.append(short) 0228 self.short2long[short[0]] = long 0229 0230 # for option_table 0231 0232 # _grok_option_table() 0233 0234 0235 def getopt (self, args=None, object=None): 0236 """Parse command-line options in args. Store as attributes on object. 0237 0238 If 'args' is None or not supplied, uses 'sys.argv[1:]'. If 0239 'object' is None or not supplied, creates a new OptionDummy 0240 object, stores option values there, and returns a tuple (args, 0241 object). If 'object' is supplied, it is modified in place and 0242 'getopt()' just returns 'args'; in both cases, the returned 0243 'args' is a modified copy of the passed-in 'args' list, which 0244 is left untouched. 0245 """ 0246 if args is None: 0247 args = sys.argv[1:] 0248 if object is None: 0249 object = OptionDummy() 0250 created_object = 1 0251 else: 0252 created_object = 0 0253 0254 self._grok_option_table() 0255 0256 short_opts = string.join(self.short_opts) 0257 try: 0258 opts, args = getopt.getopt(args, short_opts, self.long_opts) 0259 except getopt.error, msg: 0260 raise DistutilsArgError, msg 0261 0262 for opt, val in opts: 0263 if len(opt) == 2 and opt[0] == '-': # it's a short option 0264 opt = self.short2long[opt[1]] 0265 else: 0266 assert len(opt) > 2 and opt[:2] == '--' 0267 opt = opt[2:] 0268 0269 alias = self.alias.get(opt) 0270 if alias: 0271 opt = alias 0272 0273 if not self.takes_arg[opt]: # boolean option? 0274 assert val == '', "boolean option can't have value" 0275 alias = self.negative_alias.get(opt) 0276 if alias: 0277 opt = alias 0278 val = 0 0279 else: 0280 val = 1 0281 0282 attr = self.attr_name[opt] 0283 # The only repeating option at the moment is 'verbose'. 0284 # It has a negative option -q quiet, which should set verbose = 0. 0285 if val and self.repeat.get(attr) is not None: 0286 val = getattr(object, attr, 0) + 1 0287 setattr(object, attr, val) 0288 self.option_order.append((opt, val)) 0289 0290 # for opts 0291 if created_object: 0292 return args, object 0293 else: 0294 return args 0295 0296 # getopt() 0297 0298 0299 def get_option_order (self): 0300 """Returns the list of (option, value) tuples processed by the 0301 previous run of 'getopt()'. Raises RuntimeError if 0302 'getopt()' hasn't been called yet. 0303 """ 0304 if self.option_order is None: 0305 raise RuntimeError, "'getopt()' hasn't been called yet" 0306 else: 0307 return self.option_order 0308 0309 0310 def generate_help (self, header=None): 0311 """Generate help text (a list of strings, one per suggested line of 0312 output) from the option table for this FancyGetopt object. 0313 """ 0314 # Blithely assume the option table is good: probably wouldn't call 0315 # 'generate_help()' unless you've already called 'getopt()'. 0316 0317 # First pass: determine maximum length of long option names 0318 max_opt = 0 0319 for option in self.option_table: 0320 long = option[0] 0321 short = option[1] 0322 l = len(long) 0323 if long[-1] == '=': 0324 l = l - 1 0325 if short is not None: 0326 l = l + 5 # " (-x)" where short == 'x' 0327 if l > max_opt: 0328 max_opt = l 0329 0330 opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter 0331 0332 # Typical help block looks like this: 0333 # --foo controls foonabulation 0334 # Help block for longest option looks like this: 0335 # --flimflam set the flim-flam level 0336 # and with wrapped text: 0337 # --flimflam set the flim-flam level (must be between 0338 # 0 and 100, except on Tuesdays) 0339 # Options with short names will have the short name shown (but 0340 # it doesn't contribute to max_opt): 0341 # --foo (-f) controls foonabulation 0342 # If adding the short option would make the left column too wide, 0343 # we push the explanation off to the next line 0344 # --flimflam (-l) 0345 # set the flim-flam level 0346 # Important parameters: 0347 # - 2 spaces before option block start lines 0348 # - 2 dashes for each long option name 0349 # - min. 2 spaces between option and explanation (gutter) 0350 # - 5 characters (incl. space) for short option name 0351 0352 # Now generate lines of help text. (If 80 columns were good enough 0353 # for Jesus, then 78 columns are good enough for me!) 0354 line_width = 78 0355 text_width = line_width - opt_width 0356 big_indent = ' ' * opt_width 0357 if header: 0358 lines = [header] 0359 else: 0360 lines = ['Option summary:'] 0361 0362 for option in self.option_table: 0363 long, short, help = option[:3] 0364 text = wrap_text(help, text_width) 0365 if long[-1] == '=': 0366 long = long[0:-1] 0367 0368 # Case 1: no short option at all (makes life easy) 0369 if short is None: 0370 if text: 0371 lines.append(" --%-*s %s" % (max_opt, long, text[0])) 0372 else: 0373 lines.append(" --%-*s " % (max_opt, long)) 0374 0375 # Case 2: we have a short option, so we have to include it 0376 # just after the long option 0377 else: 0378 opt_names = "%s (-%s)" % (long, short) 0379 if text: 0380 lines.append(" --%-*s %s" % 0381 (max_opt, opt_names, text[0])) 0382 else: 0383 lines.append(" --%-*s" % opt_names) 0384 0385 for l in text[1:]: 0386 lines.append(big_indent + l) 0387 0388 # for self.option_table 0389 0390 return lines 0391 0392 # generate_help () 0393 0394 def print_help (self, header=None, file=None): 0395 if file is None: 0396 file = sys.stdout 0397 for line in self.generate_help(header): 0398 file.write(line + "\n") 0399 0400 # class FancyGetopt 0401 0402 0403 def fancy_getopt (options, negative_opt, object, args): 0404 parser = FancyGetopt(options) 0405 parser.set_negative_aliases(negative_opt) 0406 return parser.getopt(args, object) 0407 0408 0409 WS_TRANS = string.maketrans(string.whitespace, ' ' * len(string.whitespace)) 0410 0411 def wrap_text (text, width): 0412 """wrap_text(text : string, width : int) -> [string] 0413 0414 Split 'text' into multiple lines of no more than 'width' characters 0415 each, and return the list of strings that results. 0416 """ 0417 0418 if text is None: 0419 return [] 0420 if len(text) <= width: 0421 return [text] 0422 0423 text = string.expandtabs(text) 0424 text = string.translate(text, WS_TRANS) 0425 chunks = re.split(r'( +|-+)', text) 0426 chunks = filter(None, chunks) # ' - ' results in empty strings 0427 lines = [] 0428 0429 while chunks: 0430 0431 cur_line = [] # list of chunks (to-be-joined) 0432 cur_len = 0 # length of current line 0433 0434 while chunks: 0435 l = len(chunks[0]) 0436 if cur_len + l <= width: # can squeeze (at least) this chunk in 0437 cur_line.append(chunks[0]) 0438 del chunks[0] 0439 cur_len = cur_len + l 0440 else: # this line is full 0441 # drop last chunk if all space 0442 if cur_line and cur_line[-1][0] == ' ': 0443 del cur_line[-1] 0444 break 0445 0446 if chunks: # any chunks left to process? 0447 0448 # if the current line is still empty, then we had a single 0449 # chunk that's too big too fit on a line -- so we break 0450 # down and break it up at the line width 0451 if cur_len == 0: 0452 cur_line.append(chunks[0][0:width]) 0453 chunks[0] = chunks[0][width:] 0454 0455 # all-whitespace chunks at the end of a line can be discarded 0456 # (and we know from the re.split above that if a chunk has 0457 # *any* whitespace, it is *all* whitespace) 0458 if chunks[0][0] == ' ': 0459 del chunks[0] 0460 0461 # and store this line in the list-of-all-lines -- as a single 0462 # string, of course! 0463 lines.append(string.join(cur_line, '')) 0464 0465 # while chunks 0466 0467 return lines 0468 0469 # wrap_text () 0470 0471 0472 def translate_longopt (opt): 0473 """Convert a long option name to a valid Python identifier by 0474 changing "-" to "_". 0475 """ 0476 return string.translate(opt, longopt_xlate) 0477 0478 0479 class OptionDummy: 0480 """Dummy class just used as a place to hold command-line option 0481 values as instance attributes.""" 0482 0483 def __init__ (self, options=[]): 0484 """Create a new OptionDummy instance. The attributes listed in 0485 'options' will be initialized to None.""" 0486 for opt in options: 0487 setattr(self, opt, None) 0488 0489 # class OptionDummy 0490 0491 0492 if __name__ == "__main__": 0493 text = """\ 0494 Tra-la-la, supercalifragilisticexpialidocious. 0495 How *do* you spell that odd word, anyways? 0496 (Someone ask Mary -- she'll know [or she'll 0497 say, "How should I know?"].)""" 0498 0499 for w in (10, 20, 30, 40): 0500 print "width: %d" % w 0501 print string.join(wrap_text(text, w), "\n") 0502 print 0503
Generated by PyXR 0.9.4