blob: 3110ab30dba812c068fc0ae14bae98339e882cb2 [file] [log] [blame]
Greg Ward2689e3d1999-03-22 14:52:19 +00001"""distutils.fancy_getopt
2
3Wrapper around the standard getopt module that provides the following
4additional 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# created 1999/03/03, Greg Ward
12
Greg Ward3ce77fd2000-03-02 01:49:45 +000013__revision__ = "$Id$"
Greg Ward2689e3d1999-03-22 14:52:19 +000014
Greg Ward44f8e4e1999-12-12 16:54:55 +000015import sys, string, re
Greg Ward2689e3d1999-03-22 14:52:19 +000016from types import *
17import getopt
18from 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...
Greg Warda564cc31999-10-03 20:48:53 +000024longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
25longopt_re = re.compile (r'^%s$' % longopt_pat)
26
27# For recognizing "negative alias" options, eg. "quiet=!verbose"
28neg_alias_re = re.compile ("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))
29
Greg Ward2689e3d1999-03-22 14:52:19 +000030
31# This is used to translate long options to legitimate Python identifiers
32# (for use as attributes of some object).
33longopt_xlate = string.maketrans ('-', '_')
34
35
Greg Ward44f8e4e1999-12-12 16:54:55 +000036def fancy_getopt (options, negative_opt, object, args):
Greg Ward2689e3d1999-03-22 14:52:19 +000037
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
44 # have long options.
45
46 # Build the short_opts string and long_opts list, remembering how
47 # the two are tied together
48
49 short_opts = [] # we'll join 'em when done
50 long_opts = []
51 short2long = {}
52 attr_name = {}
53 takes_arg = {}
54
Greg Ward0081cc51999-08-14 23:44:37 +000055 for option in options:
56 try:
57 (long, short, help) = option
58 except ValueError:
59 raise DistutilsGetoptError, \
60 "invalid option tuple " + str (option)
61
Greg Ward2689e3d1999-03-22 14:52:19 +000062 # Type-check the option names
63 if type (long) is not StringType or len (long) < 2:
64 raise DistutilsGetoptError, \
Greg Ward0081cc51999-08-14 23:44:37 +000065 "long option '%s' must be a string of length >= 2" % \
66 long
Greg Ward2689e3d1999-03-22 14:52:19 +000067
68 if (not ((short is None) or
69 (type (short) is StringType and len (short) == 1))):
70 raise DistutilsGetoptError, \
Greg Ward0081cc51999-08-14 23:44:37 +000071 "short option '%s' must be None or string of length 1" % \
72 short
Greg Ward2689e3d1999-03-22 14:52:19 +000073
74 long_opts.append (long)
75
76 if long[-1] == '=': # option takes an argument?
77 if short: short = short + ':'
78 long = long[0:-1]
79 takes_arg[long] = 1
80 else:
Greg Warda564cc31999-10-03 20:48:53 +000081
82 # Is option is a "negative alias" for some other option (eg.
Greg Ward44f8e4e1999-12-12 16:54:55 +000083 # "quiet" == "!verbose")?
84 alias_to = negative_opt.get(long)
85 if alias_to is not None:
Greg Warda564cc31999-10-03 20:48:53 +000086 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 " +
Greg Ward44f8e4e1999-12-12 16:54:55 +000090 "or takes an argument") % (long, alias_to)
Greg Warda564cc31999-10-03 20:48:53 +000091
Greg Warda564cc31999-10-03 20:48:53 +000092 long_opts[-1] = long
93 takes_arg[long] = 0
94
95 else:
96 takes_arg[long] = 0
97
Greg Ward2689e3d1999-03-22 14:52:19 +000098
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
106
107 attr_name[long] = string.translate (long, longopt_xlate)
108 if short:
109 short_opts.append (short)
110 short2long[short[0]] = long
111
112 # end loop over 'options'
113
114 short_opts = string.join (short_opts)
115 try:
116 (opts, args) = getopt.getopt (args, short_opts, long_opts)
117 except getopt.error, msg:
118 raise DistutilsArgError, msg
119
120 for (opt, val) in opts:
121 if len (opt) == 2 and opt[0] == '-': # it's a short option
122 opt = short2long[opt[1]]
123
124 elif len (opt) > 2 and opt[0:2] == '--':
125 opt = opt[2:]
126
127 else:
128 raise RuntimeError, "getopt lies! (bad option string '%s')" % \
129 opt
130
131 attr = attr_name[opt]
132 if takes_arg[opt]:
133 setattr (object, attr, val)
134 else:
135 if val == '':
Greg Ward44f8e4e1999-12-12 16:54:55 +0000136 alias = negative_opt.get (opt)
Greg Warda564cc31999-10-03 20:48:53 +0000137 if alias:
138 setattr (object, attr_name[alias], 0)
139 else:
140 setattr (object, attr, 1)
Greg Ward2689e3d1999-03-22 14:52:19 +0000141 else:
142 raise RuntimeError, "getopt lies! (bad value '%s')" % value
143
144 # end loop over options found in 'args'
145
146 return args
147
Greg Ward44f8e4e1999-12-12 16:54:55 +0000148# fancy_getopt()
149
150
151WS_TRANS = string.maketrans (string.whitespace, ' ' * len (string.whitespace))
152
153def wrap_text (text, width):
154
155 if text is None:
156 return []
157 if len (text) <= width:
158 return [text]
159
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
164 lines = []
165
166 while chunks:
167
168 cur_line = [] # list of chunks (to-be-joined)
169 cur_len = 0 # length of current line
170
171 while chunks:
172 l = len (chunks[0])
173 if cur_len + l <= width: # can squeeze (at least) this chunk in
174 cur_line.append (chunks[0])
175 del 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] == ' ':
180 del cur_line[-1]
181 break
182
183 if chunks: # any chunks left to process?
184
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
188 if cur_len == 0:
189 cur_line.append (chunks[0][0:width])
190 chunks[0] = chunks[0][width:]
191
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] == ' ':
196 del chunks[0]
197
198 # and store this line in the list-of-all-lines -- as a single
199 # string, of course!
200 lines.append (string.join (cur_line, ''))
201
202 # while chunks
203
204 return lines
205
206# wrap_text ()
207
208
209def generate_help (options, header=None):
210 """Generate help text (a list of strings, one per suggested line of
211 output) from an option table."""
212
213 # Blithely assume the option table is good: probably wouldn't call
214 # 'generate_help()' unless you've already called 'fancy_getopt()'.
215
216 # First pass: determine maximum length of long option names
217 max_opt = 0
218 for option in options:
219 long = option[0]
220 short = option[1]
221 l = len (long)
222 if long[-1] == '=':
223 l = l - 1
224 if short is not None:
225 l = l + 5 # " (-x)" where short == 'x'
226 if l > max_opt:
227 max_opt = l
228
229 opt_width = max_opt + 2 + 2 + 2 # room for indent + dashes + gutter
230
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
243 # --flimflam (-l)
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
250
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
255 if header:
256 lines = [header]
257 else:
258 lines = ['Option summary:']
259
260 for (long,short,help) in options:
261
262 text = wrap_text (help, text_width)
263 if long[-1] == '=':
264 long = long[0:-1]
265
266 # Case 1: no short option at all (makes life easy)
267 if short is None:
268 if text:
269 lines.append (" --%-*s %s" % (max_opt, long, text[0]))
270 else:
271 lines.append (" --%-*s " % (max_opt, long))
272
273 for l in text[1:]:
274 lines.append (big_indent + l)
275
276 # Case 2: we have a short option, so we have to include it
277 # just after the long option
278 else:
279 opt_names = "%s (-%s)" % (long, short)
280 if text:
281 lines.append (" --%-*s %s" %
282 (max_opt, opt_names, text[0]))
283 else:
284 lines.append (" --%-*s" % opt_names)
285
286 # for loop over options
287
288 return lines
289
290# generate_help ()
291
292
293def print_help (options, file=None, header=None):
294 if file is None:
295 file = sys.stdout
296 for line in generate_help (options, header):
297 file.write (line + "\n")
298# print_help ()
299
300
301if __name__ == "__main__":
302 text = """\
303Tra-la-la, supercalifragilisticexpialidocious.
304How *do* you spell that odd word, anyways?
305(Someone ask Mary -- she'll know [or she'll
306say, "How should I know?"].)"""
307
308 for w in (10, 20, 30, 40):
309 print "width: %d" % w
310 print string.join (wrap_text (text, w), "\n")
311 print