]>
git.saurik.com Git - wxWidgets.git/blob - wxPython/distutils/fancy_getopt.py
   1 """distutils.fancy_getopt 
   3 Wrapper around the standard getopt module that provides the following 
   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 
  11 # This module should be kept compatible with Python 1.5.2. 
  15 import sys
, string
, re
 
  18 from distutils
.errors 
import * 
  20 # Much like command_re in distutils.core, this is close to but not quite 
  21 # the same as a Python NAME -- except, in the spirit of most GNU 
  22 # utilities, we use '-' in place of '_'.  (The spirit of LISP lives on!) 
  23 # The similarities to NAME are again not a coincidence... 
  24 longopt_pat 
= r
'[a-zA-Z](?:[a-zA-Z0-9-]*)' 
  25 longopt_re 
= re
.compile(r
'^%s$' % longopt_pat
) 
  27 # For recognizing "negative alias" options, eg. "quiet=!verbose" 
  28 neg_alias_re 
= re
.compile("^(%s)=!(%s)$" % (longopt_pat
, longopt_pat
)) 
  30 # This is used to translate long options to legitimate Python identifiers 
  31 # (for use as attributes of some object). 
  32 longopt_xlate 
= string
.maketrans('-', '_') 
  35     """Wrapper around the standard 'getopt()' module that provides some 
  36     handy extra functionality: 
  37       * short and long options are tied together 
  38       * options have help strings, and help text can be assembled 
  40       * options set attributes of a passed-in object 
  41       * boolean options can have "negative aliases" -- eg. if 
  42         --quiet is the "negative alias" of --verbose, then "--quiet" 
  43         on the command line sets 'verbose' to false 
  46     def __init__ (self
, option_table
=None): 
  48         # The option table is (currently) a list of 3-tuples: 
  49         #   (long_option, short_option, help_string) 
  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
 
  57         # 'option_index' maps long option names to entries in the option 
  58         # table (ie. those 3-tuples). 
  59         self
.option_index 
= {} 
  63         # 'alias' records (duh) alias options; {'foo': 'bar'} means 
  64         # --foo is an alias for --bar 
  67         # 'negative_alias' keeps track of options that are the boolean 
  68         # opposite of some other option 
  69         self
.negative_alias 
= {} 
  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. 
  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 
= [] 
  89     def _build_index (self
): 
  90         self
.option_index
.clear() 
  91         for option 
in self
.option_table
: 
  92             self
.option_index
[option
[0]] = option
 
  94     def set_option_table (self
, option_table
): 
  95         self
.option_table 
= option_table
 
  98     def add_option (self
, long_option
, short_option
=None, help_string
=None): 
  99         if self
.option_index
.has_key(long_option
): 
 100             raise DistutilsGetoptError
, \
 
 101                   "option conflict: already an option '%s'" % long_option
 
 103             option 
= (long_option
, short_option
, help_string
) 
 104             self
.option_table
.append(option
) 
 105             self
.option_index
[long_option
] = option
 
 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 self
.option_index
.has_key(long_option
) 
 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 
 117         return string
.translate(long_option
, longopt_xlate
) 
 120     def _check_alias_dict (self
, aliases
, what
): 
 121         assert type(aliases
) is DictionaryType
 
 122         for (alias
, opt
) in aliases
.items(): 
 123             if not self
.option_index
.has_key(alias
): 
 124                 raise DistutilsGetoptError
, \
 
 126                        "option '%s' not defined") % (what
, alias
, alias
) 
 127             if not self
.option_index
.has_key(opt
): 
 128                 raise DistutilsGetoptError
, \
 
 130                        "aliased option '%s' not defined") % (what
, alias
, opt
) 
 132     def set_aliases (self
, alias
): 
 133         """Set the aliases for this option parser.""" 
 134         self
._check
_alias
_dict
(alias
, "alias") 
 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
 
 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 
 153         self
.short2long
.clear() 
 156         for option 
in self
.option_table
: 
 158                 long, short
, help = option
 
 160             elif len(option
) == 4: 
 161                 long, short
, help, repeat 
= option
 
 163                 # the option table is part of the code, so simply 
 164                 # assert that it is correct 
 165                 assert "invalid option tuple: %s" % `option`
 
 167             # Type- and value-check the option names 
 168             if type(long) is not StringType 
or len(long) < 2: 
 169                 raise DistutilsGetoptError
, \
 
 170                       ("invalid long option '%s': " 
 171                        "must be a string of length >= 2") % long 
 173             if (not ((short 
is None) or 
 174                      (type(short
) is StringType 
and len(short
) == 1))): 
 175                 raise DistutilsGetoptError
, \
 
 176                       ("invalid short option '%s': " 
 177                        "must a single character or None") % short
 
 179             self
.repeat
[long] = repeat
 
 180             self
.long_opts
.append(long) 
 182             if long[-1] == '=':             # option takes an argument? 
 183                 if short
: short 
= short 
+ ':' 
 185                 self
.takes_arg
[long] = 1 
 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") % \
 
 198                     self
.long_opts
[-1] = long # XXX redundant?! 
 199                     self
.takes_arg
[long] = 0 
 202                     self
.takes_arg
[long] = 0 
 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
) 
 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 
 219             if not longopt_re
.match(long): 
 220                 raise DistutilsGetoptError
, \
 
 221                       ("invalid long option name '%s' " + 
 222                        "(must be letters, numbers, hyphens only") % long 
 224             self
.attr_name
[long] = self
.get_attr_name(long) 
 226                 self
.short_opts
.append(short
) 
 227                 self
.short2long
[short
[0]] = long 
 231     # _grok_option_table() 
 234     def getopt (self
, args
=None, object=None): 
 235         """Parse command-line options in args. Store as attributes on object. 
 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 
 248             object = OptionDummy() 
 253         self
._grok
_option
_table
() 
 255         short_opts 
= string
.join(self
.short_opts
) 
 257             opts
, args 
= getopt
.getopt(args
, short_opts
, self
.long_opts
) 
 258         except getopt
.error
, msg
: 
 259             raise DistutilsArgError
, msg
 
 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]] 
 265                 assert len(opt
) > 2 and opt
[:2] == '--' 
 268             alias 
= self
.alias
.get(opt
) 
 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
) 
 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
)) 
 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. 
 303         if self
.option_order 
is None: 
 304             raise RuntimeError, "'getopt()' hasn't been called yet" 
 306             return self
.option_order
 
 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. 
 313         # Blithely assume the option table is good: probably wouldn't call 
 314         # 'generate_help()' unless you've already called 'getopt()'. 
 316         # First pass: determine maximum length of long option names 
 318         for option 
in self
.option_table
: 
 324             if short 
is not None: 
 325                 l 
= l 
+ 5                   # " (-x)" where short == 'x' 
 329         opt_width 
= max_opt 
+ 2 + 2 + 2     # room for indent + dashes + gutter 
 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 
 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 
 351         # Now generate lines of help text.  (If 80 columns were good enough 
 352         # for Jesus, then 78 columns are good enough for me!) 
 354         text_width 
= line_width 
- opt_width
 
 355         big_indent 
= ' ' * opt_width
 
 359             lines 
= ['Option summary:'] 
 361         for option 
in self
.option_table
: 
 362             long, short
, help = option
[:3] 
 363             text 
= wrap_text(help, text_width
) 
 367             # Case 1: no short option at all (makes life easy) 
 370                     lines
.append("  --%-*s  %s" % (max_opt
, long, text
[0])) 
 372                     lines
.append("  --%-*s  " % (max_opt
, long)) 
 374             # Case 2: we have a short option, so we have to include it 
 375             # just after the long option 
 377                 opt_names 
= "%s (-%s)" % (long, short
) 
 379                     lines
.append("  --%-*s  %s" % 
 380                                  (max_opt
, opt_names
, text
[0])) 
 382                     lines
.append("  --%-*s" % opt_names
) 
 385                 lines
.append(big_indent 
+ l
) 
 387         # for self.option_table 
 393     def print_help (self
, header
=None, file=None): 
 396         for line 
in self
.generate_help(header
): 
 397             file.write(line 
+ "\n") 
 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) 
 408 WS_TRANS 
= string
.maketrans(string
.whitespace
, ' ' * len(string
.whitespace
)) 
 410 def wrap_text (text
, width
): 
 411     """wrap_text(text : string, width : int) -> [string] 
 413     Split 'text' into multiple lines of no more than 'width' characters 
 414     each, and return the list of strings that results. 
 419     if len(text
) <= width
: 
 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 
 430         cur_line 
= []                   # list of chunks (to-be-joined) 
 431         cur_len 
= 0                     # length of current line 
 435             if cur_len 
+ l 
<= width
:    # can squeeze (at least) this chunk in 
 436                 cur_line
.append(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] == ' ': 
 445         if chunks
:                      # any chunks left to process? 
 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 
 451                 cur_line
.append(chunks
[0][0:width
]) 
 452                 chunks
[0] = chunks
[0][width
:] 
 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] == ' ': 
 460         # and store this line in the list-of-all-lines -- as a single 
 462         lines
.append(string
.join(cur_line
, '')) 
 471 def translate_longopt (opt
): 
 472     """Convert a long option name to a valid Python identifier by 
 475     return string
.translate(opt
, longopt_xlate
) 
 479     """Dummy class just used as a place to hold command-line option 
 480     values as instance attributes.""" 
 482     def __init__ (self
, options
=[]): 
 483         """Create a new OptionDummy instance.  The attributes listed in 
 484         'options' will be initialized to None.""" 
 486             setattr(self
, opt
, None) 
 491 if __name__ 
== "__main__": 
 493 Tra-la-la, supercalifragilisticexpialidocious. 
 494 How *do* you spell that odd word, anyways? 
 495 (Someone ask Mary -- she'll know [or she'll 
 496 say, "How should I know?"].)""" 
 498     for w 
in (10, 20, 30, 40): 
 499         print "width: %d" % w
 
 500         print string
.join(wrap_text(text
, w
), "\n")