PyXR

c:\python24\lib \ distutils \ fancy_getopt.py



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
SourceForge.net Logo