]>
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")