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 2.1.
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 tuples. The
49 # tuples may have 3 or four values:
50 # (long_option, short_option, help_string [, repeatable])
51 # if an option takes an argument, its long_option should have '='
52 # appended; short_option should just be a single character, no ':'
53 # in any case. If a long_option doesn't have a corresponding
54 # short_option, short_option should be None. All option tuples
55 # must have long options.
56 self
.option_table
= option_table
58 # 'option_index' maps long option names to entries in the option
59 # table (ie. those 3-tuples).
60 self
.option_index
= {}
64 # 'alias' records (duh) alias options; {'foo': 'bar'} means
65 # --foo is an alias for --bar
68 # 'negative_alias' keeps track of options that are the boolean
69 # opposite of some other option
70 self
.negative_alias
= {}
72 # These keep track of the information in the option table. We
73 # don't actually populate these structures until we're ready to
74 # parse the command-line, since the 'option_table' passed in here
75 # isn't necessarily the final word.
82 # And 'option_order' is filled up in 'getopt()'; it records the
83 # original order of options (and their values) on the command-line,
84 # but expands short options, converts aliases, etc.
85 self
.option_order
= []
90 def _build_index (self
):
91 self
.option_index
.clear()
92 for option
in self
.option_table
:
93 self
.option_index
[option
[0]] = option
95 def set_option_table (self
, option_table
):
96 self
.option_table
= option_table
99 def add_option (self
, long_option
, short_option
=None, help_string
=None):
100 if self
.option_index
.has_key(long_option
):
101 raise DistutilsGetoptError
, \
102 "option conflict: already an option '%s'" % long_option
104 option
= (long_option
, short_option
, help_string
)
105 self
.option_table
.append(option
)
106 self
.option_index
[long_option
] = option
109 def has_option (self
, long_option
):
110 """Return true if the option table for this parser has an
111 option with long name 'long_option'."""
112 return self
.option_index
.has_key(long_option
)
114 def get_attr_name (self
, long_option
):
115 """Translate long option name 'long_option' to the form it
116 has as an attribute of some object: ie., translate hyphens
118 return string
.translate(long_option
, longopt_xlate
)
121 def _check_alias_dict (self
, aliases
, what
):
122 assert type(aliases
) is DictionaryType
123 for (alias
, opt
) in aliases
.items():
124 if not self
.option_index
.has_key(alias
):
125 raise DistutilsGetoptError
, \
127 "option '%s' not defined") % (what
, alias
, alias
)
128 if not self
.option_index
.has_key(opt
):
129 raise DistutilsGetoptError
, \
131 "aliased option '%s' not defined") % (what
, alias
, opt
)
133 def set_aliases (self
, alias
):
134 """Set the aliases for this option parser."""
135 self
._check
_alias
_dict
(alias
, "alias")
138 def set_negative_aliases (self
, negative_alias
):
139 """Set the negative aliases for this option parser.
140 'negative_alias' should be a dictionary mapping option names to
141 option names, both the key and value must already be defined
142 in the option table."""
143 self
._check
_alias
_dict
(negative_alias
, "negative alias")
144 self
.negative_alias
= negative_alias
147 def _grok_option_table (self
):
148 """Populate the various data structures that keep tabs on the
149 option table. Called by 'getopt()' before it can do anything
154 self
.short2long
.clear()
157 for option
in self
.option_table
:
159 long, short
, help = option
161 elif len(option
) == 4:
162 long, short
, help, repeat
= option
164 # the option table is part of the code, so simply
165 # assert that it is correct
166 raise ValueError, "invalid option tuple: %r" % (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
.repeat
[long] = repeat
181 self
.long_opts
.append(long)
183 if long[-1] == '=': # option takes an argument?
184 if short
: short
= short
+ ':'
186 self
.takes_arg
[long] = 1
189 # Is option is a "negative alias" for some other option (eg.
190 # "quiet" == "!verbose")?
191 alias_to
= self
.negative_alias
.get(long)
192 if alias_to
is not None:
193 if self
.takes_arg
[alias_to
]:
194 raise DistutilsGetoptError
, \
195 ("invalid negative alias '%s': "
196 "aliased option '%s' takes a value") % \
199 self
.long_opts
[-1] = long # XXX redundant?!
200 self
.takes_arg
[long] = 0
203 self
.takes_arg
[long] = 0
205 # If this is an alias option, make sure its "takes arg" flag is
206 # the same as the option it's aliased to.
207 alias_to
= self
.alias
.get(long)
208 if alias_to
is not None:
209 if self
.takes_arg
[long] != self
.takes_arg
[alias_to
]:
210 raise DistutilsGetoptError
, \
211 ("invalid alias '%s': inconsistent with "
212 "aliased option '%s' (one of them takes a value, "
213 "the other doesn't") % (long, alias_to
)
216 # Now enforce some bondage on the long option name, so we can
217 # later translate it to an attribute name on some object. Have
218 # to do this a bit late to make sure we've removed any trailing
220 if not longopt_re
.match(long):
221 raise DistutilsGetoptError
, \
222 ("invalid long option name '%s' " +
223 "(must be letters, numbers, hyphens only") % long
225 self
.attr_name
[long] = self
.get_attr_name(long)
227 self
.short_opts
.append(short
)
228 self
.short2long
[short
[0]] = long
232 # _grok_option_table()
235 def getopt (self
, args
=None, object=None):
236 """Parse command-line options in args. Store as attributes on object.
238 If 'args' is None or not supplied, uses 'sys.argv[1:]'. If
239 'object' is None or not supplied, creates a new OptionDummy
240 object, stores option values there, and returns a tuple (args,
241 object). If 'object' is supplied, it is modified in place and
242 'getopt()' just returns 'args'; in both cases, the returned
243 'args' is a modified copy of the passed-in 'args' list, which
249 object = OptionDummy()
254 self
._grok
_option
_table
()
256 short_opts
= string
.join(self
.short_opts
)
258 opts
, args
= getopt
.getopt(args
, short_opts
, self
.long_opts
)
259 except getopt
.error
, msg
:
260 raise DistutilsArgError
, msg
262 for opt
, val
in opts
:
263 if len(opt
) == 2 and opt
[0] == '-': # it's a short option
264 opt
= self
.short2long
[opt
[1]]
266 assert len(opt
) > 2 and opt
[:2] == '--'
269 alias
= self
.alias
.get(opt
)
273 if not self
.takes_arg
[opt
]: # boolean option?
274 assert val
== '', "boolean option can't have value"
275 alias
= self
.negative_alias
.get(opt
)
282 attr
= self
.attr_name
[opt
]
283 # The only repeating option at the moment is 'verbose'.
284 # It has a negative option -q quiet, which should set verbose = 0.
285 if val
and self
.repeat
.get(attr
) is not None:
286 val
= getattr(object, attr
, 0) + 1
287 setattr(object, attr
, val
)
288 self
.option_order
.append((opt
, val
))
299 def get_option_order (self
):
300 """Returns the list of (option, value) tuples processed by the
301 previous run of 'getopt()'. Raises RuntimeError if
302 'getopt()' hasn't been called yet.
304 if self
.option_order
is None:
305 raise RuntimeError, "'getopt()' hasn't been called yet"
307 return self
.option_order
310 def generate_help (self
, header
=None):
311 """Generate help text (a list of strings, one per suggested line of
312 output) from the option table for this FancyGetopt object.
314 # Blithely assume the option table is good: probably wouldn't call
315 # 'generate_help()' unless you've already called 'getopt()'.
317 # First pass: determine maximum length of long option names
319 for option
in self
.option_table
:
325 if short
is not None:
326 l
= l
+ 5 # " (-x)" where short == 'x'
330 opt_width
= max_opt
+ 2 + 2 + 2 # room for indent + dashes + gutter
332 # Typical help block looks like this:
333 # --foo controls foonabulation
334 # Help block for longest option looks like this:
335 # --flimflam set the flim-flam level
336 # and with wrapped text:
337 # --flimflam set the flim-flam level (must be between
338 # 0 and 100, except on Tuesdays)
339 # Options with short names will have the short name shown (but
340 # it doesn't contribute to max_opt):
341 # --foo (-f) controls foonabulation
342 # If adding the short option would make the left column too wide,
343 # we push the explanation off to the next line
345 # set the flim-flam level
346 # Important parameters:
347 # - 2 spaces before option block start lines
348 # - 2 dashes for each long option name
349 # - min. 2 spaces between option and explanation (gutter)
350 # - 5 characters (incl. space) for short option name
352 # Now generate lines of help text. (If 80 columns were good enough
353 # for Jesus, then 78 columns are good enough for me!)
355 text_width
= line_width
- opt_width
356 big_indent
= ' ' * opt_width
360 lines
= ['Option summary:']
362 for option
in self
.option_table
:
363 long, short
, help = option
[:3]
364 text
= wrap_text(help, text_width
)
368 # Case 1: no short option at all (makes life easy)
371 lines
.append(" --%-*s %s" % (max_opt
, long, text
[0]))
373 lines
.append(" --%-*s " % (max_opt
, long))
375 # Case 2: we have a short option, so we have to include it
376 # just after the long option
378 opt_names
= "%s (-%s)" % (long, short
)
380 lines
.append(" --%-*s %s" %
381 (max_opt
, opt_names
, text
[0]))
383 lines
.append(" --%-*s" % opt_names
)
386 lines
.append(big_indent
+ l
)
388 # for self.option_table
394 def print_help (self
, header
=None, file=None):
397 for line
in self
.generate_help(header
):
398 file.write(line
+ "\n")
403 def fancy_getopt (options
, negative_opt
, object, args
):
404 parser
= FancyGetopt(options
)
405 parser
.set_negative_aliases(negative_opt
)
406 return parser
.getopt(args
, object)
409 WS_TRANS
= string
.maketrans(string
.whitespace
, ' ' * len(string
.whitespace
))
411 def wrap_text (text
, width
):
412 """wrap_text(text : string, width : int) -> [string]
414 Split 'text' into multiple lines of no more than 'width' characters
415 each, and return the list of strings that results.
420 if len(text
) <= width
:
423 text
= string
.expandtabs(text
)
424 text
= string
.translate(text
, WS_TRANS
)
425 chunks
= re
.split(r
'( +|-+)', text
)
426 chunks
= filter(None, chunks
) # ' - ' results in empty strings
431 cur_line
= [] # list of chunks (to-be-joined)
432 cur_len
= 0 # length of current line
436 if cur_len
+ l
<= width
: # can squeeze (at least) this chunk in
437 cur_line
.append(chunks
[0])
439 cur_len
= cur_len
+ l
440 else: # this line is full
441 # drop last chunk if all space
442 if cur_line
and cur_line
[-1][0] == ' ':
446 if chunks
: # any chunks left to process?
448 # if the current line is still empty, then we had a single
449 # chunk that's too big too fit on a line -- so we break
450 # down and break it up at the line width
452 cur_line
.append(chunks
[0][0:width
])
453 chunks
[0] = chunks
[0][width
:]
455 # all-whitespace chunks at the end of a line can be discarded
456 # (and we know from the re.split above that if a chunk has
457 # *any* whitespace, it is *all* whitespace)
458 if chunks
[0][0] == ' ':
461 # and store this line in the list-of-all-lines -- as a single
463 lines
.append(string
.join(cur_line
, ''))
472 def translate_longopt (opt
):
473 """Convert a long option name to a valid Python identifier by
476 return string
.translate(opt
, longopt_xlate
)
480 """Dummy class just used as a place to hold command-line option
481 values as instance attributes."""
483 def __init__ (self
, options
=[]):
484 """Create a new OptionDummy instance. The attributes listed in
485 'options' will be initialized to None."""
487 setattr(self
, opt
, None)
492 if __name__
== "__main__":
494 Tra-la-la, supercalifragilisticexpialidocious.
495 How *do* you spell that odd word, anyways?
496 (Someone ask Mary -- she'll know [or she'll
497 say, "How should I know?"].)"""
499 for w
in (10, 20, 30, 40):
500 print "width: %d" % w
501 print string
.join(wrap_text(text
, w
), "\n")