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
13 import sys
, string
, re
16 from distutils
.errors
import *
18 # Much like command_re in distutils.core, this is close to but not quite
19 # the same as a Python NAME -- except, in the spirit of most GNU
20 # utilities, we use '-' in place of '_'. (The spirit of LISP lives on!)
21 # The similarities to NAME are again not a coincidence...
22 longopt_pat
= r
'[a-zA-Z](?:[a-zA-Z0-9-]*)'
23 longopt_re
= re
.compile(r
'^%s$' % longopt_pat
)
25 # For recognizing "negative alias" options, eg. "quiet=!verbose"
26 neg_alias_re
= re
.compile("^(%s)=!(%s)$" % (longopt_pat
, longopt_pat
))
28 # This is used to translate long options to legitimate Python identifiers
29 # (for use as attributes of some object).
30 longopt_xlate
= string
.maketrans('-', '_')
33 """Wrapper around the standard 'getopt()' module that provides some
34 handy extra functionality:
35 * short and long options are tied together
36 * options have help strings, and help text can be assembled
38 * options set attributes of a passed-in object
39 * boolean options can have "negative aliases" -- eg. if
40 --quiet is the "negative alias" of --verbose, then "--quiet"
41 on the command line sets 'verbose' to false
44 def __init__ (self
, option_table
=None):
46 # The option table is (currently) a list of tuples. The
47 # tuples may have 3 or four values:
48 # (long_option, short_option, help_string [, repeatable])
49 # if an option takes an argument, its long_option should have '='
50 # appended; short_option should just be a single character, no ':'
51 # in any case. If a long_option doesn't have a corresponding
52 # short_option, short_option should be None. All option tuples
53 # must have long options.
54 self
.option_table
= option_table
56 # 'option_index' maps long option names to entries in the option
57 # table (ie. those 3-tuples).
58 self
.option_index
= {}
62 # 'alias' records (duh) alias options; {'foo': 'bar'} means
63 # --foo is an alias for --bar
66 # 'negative_alias' keeps track of options that are the boolean
67 # opposite of some other option
68 self
.negative_alias
= {}
70 # These keep track of the information in the option table. We
71 # don't actually populate these structures until we're ready to
72 # parse the command-line, since the 'option_table' passed in here
73 # isn't necessarily the final word.
80 # And 'option_order' is filled up in 'getopt()'; it records the
81 # original order of options (and their values) on the command-line,
82 # but expands short options, converts aliases, etc.
83 self
.option_order
= []
88 def _build_index (self
):
89 self
.option_index
.clear()
90 for option
in self
.option_table
:
91 self
.option_index
[option
[0]] = option
93 def set_option_table (self
, option_table
):
94 self
.option_table
= option_table
97 def add_option (self
, long_option
, short_option
=None, help_string
=None):
98 if long_option
in self
.option_index
:
99 raise DistutilsGetoptError
, \
100 "option conflict: already an option '%s'" % long_option
102 option
= (long_option
, short_option
, help_string
)
103 self
.option_table
.append(option
)
104 self
.option_index
[long_option
] = option
107 def has_option (self
, long_option
):
108 """Return true if the option table for this parser has an
109 option with long name 'long_option'."""
110 return long_option
in self
.option_index
112 def get_attr_name (self
, long_option
):
113 """Translate long option name 'long_option' to the form it
114 has as an attribute of some object: ie., translate hyphens
116 return string
.translate(long_option
, longopt_xlate
)
119 def _check_alias_dict (self
, aliases
, what
):
120 assert type(aliases
) is DictionaryType
121 for (alias
, opt
) in aliases
.items():
122 if alias
not in self
.option_index
:
123 raise DistutilsGetoptError
, \
125 "option '%s' not defined") % (what
, alias
, alias
)
126 if opt
not in self
.option_index
:
127 raise DistutilsGetoptError
, \
129 "aliased option '%s' not defined") % (what
, alias
, opt
)
131 def set_aliases (self
, alias
):
132 """Set the aliases for this option parser."""
133 self
._check
_alias
_dict
(alias
, "alias")
136 def set_negative_aliases (self
, negative_alias
):
137 """Set the negative aliases for this option parser.
138 'negative_alias' should be a dictionary mapping option names to
139 option names, both the key and value must already be defined
140 in the option table."""
141 self
._check
_alias
_dict
(negative_alias
, "negative alias")
142 self
.negative_alias
= negative_alias
145 def _grok_option_table (self
):
146 """Populate the various data structures that keep tabs on the
147 option table. Called by 'getopt()' before it can do anything
152 self
.short2long
.clear()
155 for option
in self
.option_table
:
157 long, short
, help = option
159 elif len(option
) == 4:
160 long, short
, help, repeat
= option
162 # the option table is part of the code, so simply
163 # assert that it is correct
164 raise ValueError, "invalid option tuple: %r" % (option
,)
166 # Type- and value-check the option names
167 if type(long) is not StringType
or len(long) < 2:
168 raise DistutilsGetoptError
, \
169 ("invalid long option '%s': "
170 "must be a string of length >= 2") % long
172 if (not ((short
is None) or
173 (type(short
) is StringType
and len(short
) == 1))):
174 raise DistutilsGetoptError
, \
175 ("invalid short option '%s': "
176 "must a single character or None") % short
178 self
.repeat
[long] = repeat
179 self
.long_opts
.append(long)
181 if long[-1] == '=': # option takes an argument?
182 if short
: short
= short
+ ':'
184 self
.takes_arg
[long] = 1
187 # Is option is a "negative alias" for some other option (eg.
188 # "quiet" == "!verbose")?
189 alias_to
= self
.negative_alias
.get(long)
190 if alias_to
is not None:
191 if self
.takes_arg
[alias_to
]:
192 raise DistutilsGetoptError
, \
193 ("invalid negative alias '%s': "
194 "aliased option '%s' takes a value") % \
197 self
.long_opts
[-1] = long # XXX redundant?!
198 self
.takes_arg
[long] = 0
201 self
.takes_arg
[long] = 0
203 # If this is an alias option, make sure its "takes arg" flag is
204 # the same as the option it's aliased to.
205 alias_to
= self
.alias
.get(long)
206 if alias_to
is not None:
207 if self
.takes_arg
[long] != self
.takes_arg
[alias_to
]:
208 raise DistutilsGetoptError
, \
209 ("invalid alias '%s': inconsistent with "
210 "aliased option '%s' (one of them takes a value, "
211 "the other doesn't") % (long, alias_to
)
214 # Now enforce some bondage on the long option name, so we can
215 # later translate it to an attribute name on some object. Have
216 # to do this a bit late to make sure we've removed any trailing
218 if not longopt_re
.match(long):
219 raise DistutilsGetoptError
, \
220 ("invalid long option name '%s' " +
221 "(must be letters, numbers, hyphens only") % long
223 self
.attr_name
[long] = self
.get_attr_name(long)
225 self
.short_opts
.append(short
)
226 self
.short2long
[short
[0]] = long
230 # _grok_option_table()
233 def getopt (self
, args
=None, object=None):
234 """Parse command-line options in args. Store as attributes on object.
236 If 'args' is None or not supplied, uses 'sys.argv[1:]'. If
237 'object' is None or not supplied, creates a new OptionDummy
238 object, stores option values there, and returns a tuple (args,
239 object). If 'object' is supplied, it is modified in place and
240 'getopt()' just returns 'args'; in both cases, the returned
241 'args' is a modified copy of the passed-in 'args' list, which
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 assert len(opt
) > 2 and opt
[:2] == '--'
267 alias
= self
.alias
.get(opt
)
271 if not self
.takes_arg
[opt
]: # boolean option?
272 assert val
== '', "boolean option can't have value"
273 alias
= self
.negative_alias
.get(opt
)
280 attr
= self
.attr_name
[opt
]
281 # The only repeating option at the moment is 'verbose'.
282 # It has a negative option -q quiet, which should set verbose = 0.
283 if val
and self
.repeat
.get(attr
) is not None:
284 val
= getattr(object, attr
, 0) + 1
285 setattr(object, attr
, val
)
286 self
.option_order
.append((opt
, val
))
297 def get_option_order (self
):
298 """Returns the list of (option, value) tuples processed by the
299 previous run of 'getopt()'. Raises RuntimeError if
300 'getopt()' hasn't been called yet.
302 if self
.option_order
is None:
303 raise RuntimeError, "'getopt()' hasn't been called yet"
305 return self
.option_order
308 def generate_help (self
, header
=None):
309 """Generate help text (a list of strings, one per suggested line of
310 output) from the option table for this FancyGetopt object.
312 # Blithely assume the option table is good: probably wouldn't call
313 # 'generate_help()' unless you've already called 'getopt()'.
315 # First pass: determine maximum length of long option names
317 for option
in self
.option_table
:
323 if short
is not None:
324 l
= l
+ 5 # " (-x)" where short == 'x'
328 opt_width
= max_opt
+ 2 + 2 + 2 # room for indent + dashes + gutter
330 # Typical help block looks like this:
331 # --foo controls foonabulation
332 # Help block for longest option looks like this:
333 # --flimflam set the flim-flam level
334 # and with wrapped text:
335 # --flimflam set the flim-flam level (must be between
336 # 0 and 100, except on Tuesdays)
337 # Options with short names will have the short name shown (but
338 # it doesn't contribute to max_opt):
339 # --foo (-f) controls foonabulation
340 # If adding the short option would make the left column too wide,
341 # we push the explanation off to the next line
343 # set the flim-flam level
344 # Important parameters:
345 # - 2 spaces before option block start lines
346 # - 2 dashes for each long option name
347 # - min. 2 spaces between option and explanation (gutter)
348 # - 5 characters (incl. space) for short option name
350 # Now generate lines of help text. (If 80 columns were good enough
351 # for Jesus, then 78 columns are good enough for me!)
353 text_width
= line_width
- opt_width
354 big_indent
= ' ' * opt_width
358 lines
= ['Option summary:']
360 for option
in self
.option_table
:
361 long, short
, help = option
[:3]
362 text
= wrap_text(help, text_width
)
366 # Case 1: no short option at all (makes life easy)
369 lines
.append(" --%-*s %s" % (max_opt
, long, text
[0]))
371 lines
.append(" --%-*s " % (max_opt
, long))
373 # Case 2: we have a short option, so we have to include it
374 # just after the long option
376 opt_names
= "%s (-%s)" % (long, short
)
378 lines
.append(" --%-*s %s" %
379 (max_opt
, opt_names
, text
[0]))
381 lines
.append(" --%-*s" % opt_names
)
384 lines
.append(big_indent
+ l
)
386 # for self.option_table
392 def print_help (self
, header
=None, file=None):
395 for line
in self
.generate_help(header
):
396 file.write(line
+ "\n")
401 def fancy_getopt (options
, negative_opt
, object, args
):
402 parser
= FancyGetopt(options
)
403 parser
.set_negative_aliases(negative_opt
)
404 return parser
.getopt(args
, object)
407 WS_TRANS
= string
.maketrans(string
.whitespace
, ' ' * len(string
.whitespace
))
409 def wrap_text (text
, width
):
410 """wrap_text(text : string, width : int) -> [string]
412 Split 'text' into multiple lines of no more than 'width' characters
413 each, and return the list of strings that results.
418 if len(text
) <= width
:
421 text
= string
.expandtabs(text
)
422 text
= string
.translate(text
, WS_TRANS
)
423 chunks
= re
.split(r
'( +|-+)', text
)
424 chunks
= filter(None, chunks
) # ' - ' results in empty strings
429 cur_line
= [] # list of chunks (to-be-joined)
430 cur_len
= 0 # length of current line
434 if cur_len
+ l
<= width
: # can squeeze (at least) this chunk in
435 cur_line
.append(chunks
[0])
437 cur_len
= cur_len
+ l
438 else: # this line is full
439 # drop last chunk if all space
440 if cur_line
and cur_line
[-1][0] == ' ':
444 if chunks
: # any chunks left to process?
446 # if the current line is still empty, then we had a single
447 # chunk that's too big too fit on a line -- so we break
448 # down and break it up at the line width
450 cur_line
.append(chunks
[0][0:width
])
451 chunks
[0] = chunks
[0][width
:]
453 # all-whitespace chunks at the end of a line can be discarded
454 # (and we know from the re.split above that if a chunk has
455 # *any* whitespace, it is *all* whitespace)
456 if chunks
[0][0] == ' ':
459 # and store this line in the list-of-all-lines -- as a single
461 lines
.append(string
.join(cur_line
, ''))
470 def translate_longopt (opt
):
471 """Convert a long option name to a valid Python identifier by
474 return string
.translate(opt
, longopt_xlate
)
478 """Dummy class just used as a place to hold command-line option
479 values as instance attributes."""
481 def __init__ (self
, options
=[]):
482 """Create a new OptionDummy instance. The attributes listed in
483 'options' will be initialized to None."""
485 setattr(self
, opt
, None)
490 if __name__
== "__main__":
492 Tra-la-la, supercalifragilisticexpialidocious.
493 How *do* you spell that odd word, anyways?
494 (Someone ask Mary -- she'll know [or she'll
495 say, "How should I know?"].)"""
497 for w
in (10, 20, 30, 40):
498 print "width: %d" % w
499 print string
.join(wrap_text(text
, w
), "\n")