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 # created 1999/03/03, Greg Ward
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('-', '_')
34 # This records (option, value) pairs in the order seen on the command line;
35 # it's close to what getopt.getopt() returns, but with short options
36 # expanded. (Ugh, this module should be OO-ified.)
41 """Wrapper around the standard 'getopt()' module that provides some
42 handy extra functionality:
43 * short and long options are tied together
44 * options have help strings, and help text can be assembled
46 * options set attributes of a passed-in object
47 * boolean options can have "negative aliases" -- eg. if
48 --quiet is the "negative alias" of --verbose, then "--quiet"
49 on the command line sets 'verbose' to false
52 def __init__ (self
, option_table
=None):
54 # The option table is (currently) a list of 3-tuples:
55 # (long_option, short_option, help_string)
56 # if an option takes an argument, its long_option should have '='
57 # appended; short_option should just be a single character, no ':'
58 # in any case. If a long_option doesn't have a corresponding
59 # short_option, short_option should be None. All option tuples
60 # must have long options.
61 self
.option_table
= option_table
63 # 'option_index' maps long option names to entries in the option
64 # table (ie. those 3-tuples).
65 self
.option_index
= {}
69 # 'alias' records (duh) alias options; {'foo': 'bar'} means
70 # --foo is an alias for --bar
73 # 'negative_alias' keeps track of options that are the boolean
74 # opposite of some other option
75 self
.negative_alias
= {}
77 # These keep track of the information in the option table. We
78 # don't actually populate these structures until we're ready to
79 # parse the command-line, since the 'option_table' passed in here
80 # isn't necessarily the final word.
87 # And 'option_order' is filled up in 'getopt()'; it records the
88 # original order of options (and their values) on the command-line,
89 # but expands short options, converts aliases, etc.
90 self
.option_order
= []
95 def _build_index (self
):
96 self
.option_index
.clear()
97 for option
in self
.option_table
:
98 self
.option_index
[option
[0]] = option
100 def set_option_table (self
, option_table
):
101 self
.option_table
= option_table
104 def add_option (self
, long_option
, short_option
=None, help_string
=None):
105 if self
.option_index
.has_key(long_option
):
106 raise DistutilsGetoptError
, \
107 "option conflict: already an option '%s'" % long_option
109 option
= (long_option
, short_option
, help_string
)
110 self
.option_table
.append(option
)
111 self
.option_index
[long_option
] = option
114 def has_option (self
, long_option
):
115 """Return true if the option table for this parser has an
116 option with long name 'long_option'."""
117 return self
.option_index
.has_key(long_option
)
119 def get_attr_name (self
, long_option
):
120 """Translate long option name 'long_option' to the form it
121 has as an attribute of some object: ie., translate hyphens
123 return string
.translate(long_option
, longopt_xlate
)
126 def _check_alias_dict (self
, aliases
, what
):
127 assert type(aliases
) is DictionaryType
128 for (alias
, opt
) in aliases
.items():
129 if not self
.option_index
.has_key(alias
):
130 raise DistutilsGetoptError
, \
132 "option '%s' not defined") % (what
, alias
, alias
)
133 if not self
.option_index
.has_key(opt
):
134 raise DistutilsGetoptError
, \
136 "aliased option '%s' not defined") % (what
, alias
, opt
)
138 def set_aliases (self
, alias
):
139 """Set the aliases for this option parser."""
140 self
._check
_alias
_dict
(alias
, "alias")
143 def set_negative_aliases (self
, negative_alias
):
144 """Set the negative aliases for this option parser.
145 'negative_alias' should be a dictionary mapping option names to
146 option names, both the key and value must already be defined
147 in the option table."""
148 self
._check
_alias
_dict
(negative_alias
, "negative alias")
149 self
.negative_alias
= negative_alias
152 def _grok_option_table (self
):
153 """Populate the various data structures that keep tabs on the
154 option table. Called by 'getopt()' before it can do anything
159 self
.short2long
.clear()
161 for option
in self
.option_table
:
163 (long, short
, help) = option
165 raise DistutilsGetoptError
, \
166 "invalid option tuple " + str(option
)
168 # Type- and value-check the option names
169 if type(long) is not StringType
or len(long) < 2:
170 raise DistutilsGetoptError
, \
171 ("invalid long option '%s': "
172 "must be a string of length >= 2") % long
174 if (not ((short
is None) or
175 (type(short
) is StringType
and len(short
) == 1))):
176 raise DistutilsGetoptError
, \
177 ("invalid short option '%s': "
178 "must a single character or None") % short
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 the command-line options in 'args' and store the results
236 as attributes of 'object'. If 'args' is None or not supplied, uses
237 'sys.argv[1:]'. If 'object' is None or not supplied, creates a new
238 OptionDummy object, stores option values there, and returns a tuple
239 (args, object). If 'object' is supplied, it is modified in place
240 and 'getopt()' just returns 'args'; in both cases, the returned
241 'args' is a modified copy of the passed-in 'args' list, which is
247 object = OptionDummy()
252 self
._grok
_option
_table
()
254 short_opts
= string
.join(self
.short_opts
)
256 (opts
, args
) = getopt
.getopt(args
, short_opts
, self
.long_opts
)
257 except getopt
.error
, msg
:
258 raise DistutilsArgError
, msg
260 for (opt
, val
) in opts
:
261 if len(opt
) == 2 and opt
[0] == '-': # it's a short option
262 opt
= self
.short2long
[opt
[1]]
264 elif len(opt
) > 2 and opt
[0:2] == '--':
268 raise DistutilsInternalError
, \
269 "this can't happen: bad option string '%s'" % opt
271 alias
= self
.alias
.get(opt
)
275 if not self
.takes_arg
[opt
]: # boolean option?
276 if val
!= '': # shouldn't have a value!
277 raise DistutilsInternalError
, \
278 "this can't happen: bad option value '%s'" % val
280 alias
= self
.negative_alias
.get(opt
)
287 attr
= self
.attr_name
[opt
]
288 setattr(object, attr
, val
)
289 self
.option_order
.append((opt
, val
))
294 return (args
, object)
301 def get_option_order (self
):
302 """Returns the list of (option, value) tuples processed by the
303 previous run of 'getopt()'. Raises RuntimeError if
304 'getopt()' hasn't been called yet.
306 if self
.option_order
is None:
307 raise RuntimeError, "'getopt()' hasn't been called yet"
309 return self
.option_order
312 def generate_help (self
, header
=None):
313 """Generate help text (a list of strings, one per suggested line of
314 output) from the option table for this FancyGetopt object.
316 # Blithely assume the option table is good: probably wouldn't call
317 # 'generate_help()' unless you've already called 'getopt()'.
319 # First pass: determine maximum length of long option names
321 for option
in self
.option_table
:
327 if short
is not None:
328 l
= l
+ 5 # " (-x)" where short == 'x'
332 opt_width
= max_opt
+ 2 + 2 + 2 # room for indent + dashes + gutter
334 # Typical help block looks like this:
335 # --foo controls foonabulation
336 # Help block for longest option looks like this:
337 # --flimflam set the flim-flam level
338 # and with wrapped text:
339 # --flimflam set the flim-flam level (must be between
340 # 0 and 100, except on Tuesdays)
341 # Options with short names will have the short name shown (but
342 # it doesn't contribute to max_opt):
343 # --foo (-f) controls foonabulation
344 # If adding the short option would make the left column too wide,
345 # we push the explanation off to the next line
347 # set the flim-flam level
348 # Important parameters:
349 # - 2 spaces before option block start lines
350 # - 2 dashes for each long option name
351 # - min. 2 spaces between option and explanation (gutter)
352 # - 5 characters (incl. space) for short option name
354 # Now generate lines of help text. (If 80 columns were good enough
355 # for Jesus, then 78 columns are good enough for me!)
357 text_width
= line_width
- opt_width
358 big_indent
= ' ' * opt_width
362 lines
= ['Option summary:']
364 for (long,short
,help) in self
.option_table
:
366 text
= wrap_text(help, text_width
)
370 # Case 1: no short option at all (makes life easy)
373 lines
.append(" --%-*s %s" % (max_opt
, long, text
[0]))
375 lines
.append(" --%-*s " % (max_opt
, long))
377 # Case 2: we have a short option, so we have to include it
378 # just after the long option
380 opt_names
= "%s (-%s)" % (long, short
)
382 lines
.append(" --%-*s %s" %
383 (max_opt
, opt_names
, text
[0]))
385 lines
.append(" --%-*s" % opt_names
)
388 lines
.append(big_indent
+ l
)
390 # for self.option_table
396 def print_help (self
, header
=None, file=None):
399 for line
in self
.generate_help(header
):
400 file.write(line
+ "\n")
405 def fancy_getopt (options
, negative_opt
, object, args
):
406 parser
= FancyGetopt(options
)
407 parser
.set_negative_aliases(negative_opt
)
408 return parser
.getopt(args
, object)
411 WS_TRANS
= string
.maketrans(string
.whitespace
, ' ' * len(string
.whitespace
))
413 def wrap_text (text
, width
):
414 """wrap_text(text : string, width : int) -> [string]
416 Split 'text' into multiple lines of no more than 'width' characters
417 each, and return the list of strings that results.
422 if len(text
) <= width
:
425 text
= string
.expandtabs(text
)
426 text
= string
.translate(text
, WS_TRANS
)
427 chunks
= re
.split(r
'( +|-+)', text
)
428 chunks
= filter(None, chunks
) # ' - ' results in empty strings
433 cur_line
= [] # list of chunks (to-be-joined)
434 cur_len
= 0 # length of current line
438 if cur_len
+ l
<= width
: # can squeeze (at least) this chunk in
439 cur_line
.append(chunks
[0])
441 cur_len
= cur_len
+ l
442 else: # this line is full
443 # drop last chunk if all space
444 if cur_line
and cur_line
[-1][0] == ' ':
448 if chunks
: # any chunks left to process?
450 # if the current line is still empty, then we had a single
451 # chunk that's too big too fit on a line -- so we break
452 # down and break it up at the line width
454 cur_line
.append(chunks
[0][0:width
])
455 chunks
[0] = chunks
[0][width
:]
457 # all-whitespace chunks at the end of a line can be discarded
458 # (and we know from the re.split above that if a chunk has
459 # *any* whitespace, it is *all* whitespace)
460 if chunks
[0][0] == ' ':
463 # and store this line in the list-of-all-lines -- as a single
465 lines
.append(string
.join(cur_line
, ''))
474 def translate_longopt (opt
):
475 """Convert a long option name to a valid Python identifier by
478 return string
.translate(opt
, longopt_xlate
)
482 """Dummy class just used as a place to hold command-line option
483 values as instance attributes."""
485 def __init__ (self
, options
=[]):
486 """Create a new OptionDummy instance. The attributes listed in
487 'options' will be initialized to None."""
489 setattr(self
, opt
, None)
494 if __name__
== "__main__":
496 Tra-la-la, supercalifragilisticexpialidocious.
497 How *do* you spell that odd word, anyways?
498 (Someone ask Mary -- she'll know [or she'll
499 say, "How should I know?"].)"""
501 for w
in (10, 20, 30, 40):
502 print "width: %d" % w
503 print string
.join(wrap_text(text
, w
), "\n")