]> git.saurik.com Git - wxWidgets.git/blob - wxPython/distutils/fancy_getopt.py
Updated to current API.
[wxWidgets.git] / wxPython / distutils / fancy_getopt.py
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 # This module should be kept compatible with Python 1.5.2.
12
13 __revision__ = "$Id$"
14
15 import sys, string, re
16 from types import *
17 import getopt
18 from distutils.errors import *
19
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)
26
27 # For recognizing "negative alias" options, eg. "quiet=!verbose"
28 neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))
29
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('-', '_')
33
34 class FancyGetopt:
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
39 from them
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
44 """
45
46 def __init__ (self, option_table=None):
47
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
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 self.option_index.has_key(long_option):
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 self.option_index.has_key(long_option)
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 type(aliases) is DictionaryType
122 for (alias, opt) in aliases.items():
123 if not self.option_index.has_key(alias):
124 raise DistutilsGetoptError, \
125 ("invalid %s '%s': "
126 "option '%s' not defined") % (what, alias, alias)
127 if not self.option_index.has_key(opt):
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 assert "invalid option tuple: %s" % `option`
166
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
172
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
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 # wrap_text ()
469
470
471 def translate_longopt (opt):
472 """Convert a long option name to a valid Python identifier by
473 changing "-" to "_".
474 """
475 return string.translate(opt, longopt_xlate)
476
477
478 class OptionDummy:
479 """Dummy class just used as a place to hold command-line option
480 values as instance attributes."""
481
482 def __init__ (self, options=[]):
483 """Create a new OptionDummy instance. The attributes listed in
484 'options' will be initialized to None."""
485 for opt in options:
486 setattr(self, opt, None)
487
488 # class OptionDummy
489
490
491 if __name__ == "__main__":
492 text = """\
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?"].)"""
497
498 for w in (10, 20, 30, 40):
499 print "width: %d" % w
500 print string.join(wrap_text(text, w), "\n")
501 print