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
))
31 # This is used to translate long options to legitimate Python identifiers
32 # (for use as attributes of some object).
33 longopt_xlate
= string
.maketrans ('-', '_')
36 def fancy_getopt (options
, negative_opt
, object, args
):
38 # The 'options' table is a list of 3-tuples:
39 # (long_option, short_option, help_string)
40 # if an option takes an argument, its long_option should have '='
41 # appended; short_option should just be a single character, no ':' in
42 # any case. If a long_option doesn't have a corresponding
43 # short_option, short_option should be None. All option tuples must
46 # Build the short_opts string and long_opts list, remembering how
47 # the two are tied together
49 short_opts
= [] # we'll join 'em when done
55 for option
in options
:
57 (long, short
, help) = option
59 raise DistutilsGetoptError
, \
60 "invalid option tuple " + str (option
)
62 # Type-check the option names
63 if type (long) is not StringType
or len (long) < 2:
64 raise DistutilsGetoptError
, \
65 "long option '%s' must be a string of length >= 2" % \
68 if (not ((short
is None) or
69 (type (short
) is StringType
and len (short
) == 1))):
70 raise DistutilsGetoptError
, \
71 "short option '%s' must be None or string of length 1" % \
74 long_opts
.append (long)
76 if long[-1] == '=': # option takes an argument?
77 if short
: short
= short
+ ':'
82 # Is option is a "negative alias" for some other option (eg.
83 # "quiet" == "!verbose")?
84 alias_to
= negative_opt
.get(long)
85 if alias_to
is not None:
86 if not takes_arg
.has_key(alias_to
) or takes_arg
[alias_to
]:
87 raise DistutilsGetoptError
, \
88 ("option '%s' is a negative alias for '%s', " +
89 "which either hasn't been defined yet " +
90 "or takes an argument") % (long, alias_to
)
99 # Now enforce some bondage on the long option name, so we can later
100 # translate it to an attribute name in 'object'. Have to do this a
101 # bit late to make sure we've removed any trailing '='.
102 if not longopt_re
.match (long):
103 raise DistutilsGetoptError
, \
104 ("invalid long option name '%s' " +
105 "(must be letters, numbers, hyphens only") % long
107 attr_name
[long] = string
.translate (long, longopt_xlate
)
109 short_opts
.append (short
)
110 short2long
[short
[0]] = long
112 # end loop over 'options'
114 short_opts
= string
.join (short_opts
)
116 (opts
, args
) = getopt
.getopt (args
, short_opts
, long_opts
)
117 except getopt
.error
, msg
:
118 raise DistutilsArgError
, msg
120 for (opt
, val
) in opts
:
121 if len (opt
) == 2 and opt
[0] == '-': # it's a short option
122 opt
= short2long
[opt
[1]]
124 elif len (opt
) > 2 and opt
[0:2] == '--':
128 raise RuntimeError, "getopt lies! (bad option string '%s')" % \
131 attr
= attr_name
[opt
]
133 setattr (object, attr
, val
)
136 alias
= negative_opt
.get (opt
)
138 setattr (object, attr_name
[alias
], 0)
140 setattr (object, attr
, 1)
142 raise RuntimeError, "getopt lies! (bad value '%s')" % value
144 # end loop over options found in 'args'
151 WS_TRANS
= string
.maketrans (string
.whitespace
, ' ' * len (string
.whitespace
))
153 def wrap_text (text
, width
):
157 if len (text
) <= width
:
160 text
= string
.expandtabs (text
)
161 text
= string
.translate (text
, WS_TRANS
)
162 chunks
= re
.split (r
'( +|-+)', text
)
163 chunks
= filter (None, chunks
) # ' - ' results in empty strings
168 cur_line
= [] # list of chunks (to-be-joined)
169 cur_len
= 0 # length of current line
173 if cur_len
+ l
<= width
: # can squeeze (at least) this chunk in
174 cur_line
.append (chunks
[0])
176 cur_len
= cur_len
+ l
177 else: # this line is full
178 # drop last chunk if all space
179 if cur_line
and cur_line
[-1][0] == ' ':
183 if chunks
: # any chunks left to process?
185 # if the current line is still empty, then we had a single
186 # chunk that's too big too fit on a line -- so we break
187 # down and break it up at the line width
189 cur_line
.append (chunks
[0][0:width
])
190 chunks
[0] = chunks
[0][width
:]
192 # all-whitespace chunks at the end of a line can be discarded
193 # (and we know from the re.split above that if a chunk has
194 # *any* whitespace, it is *all* whitespace)
195 if chunks
[0][0] == ' ':
198 # and store this line in the list-of-all-lines -- as a single
200 lines
.append (string
.join (cur_line
, ''))
209 def generate_help (options
, header
=None):
210 """Generate help text (a list of strings, one per suggested line of
211 output) from an option table."""
213 # Blithely assume the option table is good: probably wouldn't call
214 # 'generate_help()' unless you've already called 'fancy_getopt()'.
216 # First pass: determine maximum length of long option names
218 for option
in options
:
224 if short
is not None:
225 l
= l
+ 5 # " (-x)" where short == 'x'
229 opt_width
= max_opt
+ 2 + 2 + 2 # room for indent + dashes + gutter
231 # Typical help block looks like this:
232 # --foo controls foonabulation
233 # Help block for longest option looks like this:
234 # --flimflam set the flim-flam level
235 # and with wrapped text:
236 # --flimflam set the flim-flam level (must be between
237 # 0 and 100, except on Tuesdays)
238 # Options with short names will have the short name shown (but
239 # it doesn't contribute to max_opt):
240 # --foo (-f) controls foonabulation
241 # If adding the short option would make the left column too wide,
242 # we push the explanation off to the next line
244 # set the flim-flam level
245 # Important parameters:
246 # - 2 spaces before option block start lines
247 # - 2 dashes for each long option name
248 # - min. 2 spaces between option and explanation (gutter)
249 # - 5 characters (incl. space) for short option name
251 # Now generate lines of help text.
252 line_width
= 78 # if 80 columns were good enough for
253 text_width
= line_width
- opt_width
# Jesus, then 78 are good enough for me
254 big_indent
= ' ' * opt_width
258 lines
= ['Option summary:']
260 for (long,short
,help) in options
:
262 text
= wrap_text (help, text_width
)
266 # Case 1: no short option at all (makes life easy)
269 lines
.append (" --%-*s %s" % (max_opt
, long, text
[0]))
271 lines
.append (" --%-*s " % (max_opt
, long))
274 lines
.append (big_indent
+ l
)
276 # Case 2: we have a short option, so we have to include it
277 # just after the long option
279 opt_names
= "%s (-%s)" % (long, short
)
281 lines
.append (" --%-*s %s" %
282 (max_opt
, opt_names
, text
[0]))
284 lines
.append (" --%-*s" % opt_names
)
286 # for loop over options
293 def print_help (options
, file=None, header
=None):
296 for line
in generate_help (options
, header
):
297 file.write (line
+ "\n")
301 if __name__
== "__main__":
303 Tra-la-la, supercalifragilisticexpialidocious.
304 How *do* you spell that odd word, anyways?
305 (Someone ask Mary -- she'll know [or she'll
306 say, "How should I know?"].)"""
308 for w
in (10, 20, 30, 40):
309 print "width: %d" % w
310 print string
.join (wrap_text (text
, w
), "\n")