eb1e2e23c388bbfdba5e6ca128dcbb4655c09821
[profile/ivi/python.git] / Lib / distutils / fancy_getopt.py
1 """distutils.fancy_getopt
2
3 Wrapper around the standard getopt module that provides the following
4 additional 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 __revision__ = "$Id: fancy_getopt.py 76956 2009-12-21 01:22:46Z tarek.ziade $"
12
13 import sys
14 import string
15 import re
16 import getopt
17 from distutils.errors import DistutilsGetoptError, DistutilsArgError
18
19 # Much like command_re in distutils.core, this is close to but not quite
20 # the same as a Python NAME -- except, in the spirit of most GNU
21 # utilities, we use '-' in place of '_'.  (The spirit of LISP lives on!)
22 # The similarities to NAME are again not a coincidence...
23 longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
24 longopt_re = re.compile(r'^%s$' % longopt_pat)
25
26 # For recognizing "negative alias" options, eg. "quiet=!verbose"
27 neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))
28
29 # This is used to translate long options to legitimate Python identifiers
30 # (for use as attributes of some object).
31 longopt_xlate = string.maketrans('-', '_')
32
33 class FancyGetopt:
34     """Wrapper around the standard 'getopt()' module that provides some
35     handy extra functionality:
36       * short and long options are tied together
37       * options have help strings, and help text can be assembled
38         from them
39       * options set attributes of a passed-in object
40       * boolean options can have "negative aliases" -- eg. if
41         --quiet is the "negative alias" of --verbose, then "--quiet"
42         on the command line sets 'verbose' to false
43     """
44
45     def __init__ (self, option_table=None):
46
47         # The option table is (currently) a list of tuples.  The
48         # tuples may have 3 or four values:
49         #   (long_option, short_option, help_string [, repeatable])
50         # if an option takes an argument, its long_option should have '='
51         # appended; short_option should just be a single character, no ':'
52         # in any case.  If a long_option doesn't have a corresponding
53         # short_option, short_option should be None.  All option tuples
54         # must have long options.
55         self.option_table = option_table
56
57         # 'option_index' maps long option names to entries in the option
58         # table (ie. those 3-tuples).
59         self.option_index = {}
60         if self.option_table:
61             self._build_index()
62
63         # 'alias' records (duh) alias options; {'foo': 'bar'} means
64         # --foo is an alias for --bar
65         self.alias = {}
66
67         # 'negative_alias' keeps track of options that are the boolean
68         # opposite of some other option
69         self.negative_alias = {}
70
71         # These keep track of the information in the option table.  We
72         # don't actually populate these structures until we're ready to
73         # parse the command-line, since the 'option_table' passed in here
74         # isn't necessarily the final word.
75         self.short_opts = []
76         self.long_opts = []
77         self.short2long = {}
78         self.attr_name = {}
79         self.takes_arg = {}
80
81         # And 'option_order' is filled up in 'getopt()'; it records the
82         # original order of options (and their values) on the command-line,
83         # but expands short options, converts aliases, etc.
84         self.option_order = []
85
86     # __init__ ()
87
88
89     def _build_index (self):
90         self.option_index.clear()
91         for option in self.option_table:
92             self.option_index[option[0]] = option
93
94     def set_option_table (self, option_table):
95         self.option_table = option_table
96         self._build_index()
97
98     def add_option (self, long_option, short_option=None, help_string=None):
99         if long_option in self.option_index:
100             raise DistutilsGetoptError, \
101                   "option conflict: already an option '%s'" % long_option
102         else:
103             option = (long_option, short_option, help_string)
104             self.option_table.append(option)
105             self.option_index[long_option] = option
106
107
108     def has_option (self, long_option):
109         """Return true if the option table for this parser has an
110         option with long name 'long_option'."""
111         return long_option in self.option_index
112
113     def get_attr_name (self, long_option):
114         """Translate long option name 'long_option' to the form it
115         has as an attribute of some object: ie., translate hyphens
116         to underscores."""
117         return string.translate(long_option, longopt_xlate)
118
119
120     def _check_alias_dict (self, aliases, what):
121         assert isinstance(aliases, dict)
122         for (alias, opt) in aliases.items():
123             if alias not in self.option_index:
124                 raise DistutilsGetoptError, \
125                       ("invalid %s '%s': "
126                        "option '%s' not defined") % (what, alias, alias)
127             if opt not in self.option_index:
128                 raise DistutilsGetoptError, \
129                       ("invalid %s '%s': "
130                        "aliased option '%s' not defined") % (what, alias, opt)
131
132     def set_aliases (self, alias):
133         """Set the aliases for this option parser."""
134         self._check_alias_dict(alias, "alias")
135         self.alias = alias
136
137     def set_negative_aliases (self, negative_alias):
138         """Set the negative aliases for this option parser.
139         'negative_alias' should be a dictionary mapping option names to
140         option names, both the key and value must already be defined
141         in the option table."""
142         self._check_alias_dict(negative_alias, "negative alias")
143         self.negative_alias = negative_alias
144
145
146     def _grok_option_table (self):
147         """Populate the various data structures that keep tabs on the
148         option table.  Called by 'getopt()' before it can do anything
149         worthwhile.
150         """
151         self.long_opts = []
152         self.short_opts = []
153         self.short2long.clear()
154         self.repeat = {}
155
156         for option in self.option_table:
157             if len(option) == 3:
158                 long, short, help = option
159                 repeat = 0
160             elif len(option) == 4:
161                 long, short, help, repeat = option
162             else:
163                 # the option table is part of the code, so simply
164                 # assert that it is correct
165                 raise ValueError, "invalid option tuple: %r" % (option,)
166
167             # Type- and value-check the option names
168             if not isinstance(long, str) or len(long) < 2:
169                 raise DistutilsGetoptError, \
170                       ("invalid long option '%s': "
171                        "must be a string of length >= 2") % long
172
173             if (not ((short is None) or
174                      (isinstance(short, str) and len(short) == 1))):
175                 raise DistutilsGetoptError, \
176                       ("invalid short option '%s': "
177                        "must a single character or None") % short
178
179             self.repeat[long] = repeat
180             self.long_opts.append(long)
181
182             if long[-1] == '=':             # option takes an argument?
183                 if short: short = short + ':'
184                 long = long[0:-1]
185                 self.takes_arg[long] = 1
186             else:
187
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") % \
196                                (long, alias_to)
197
198                     self.long_opts[-1] = long # XXX redundant?!
199                     self.takes_arg[long] = 0
200
201                 else:
202                     self.takes_arg[long] = 0
203
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)
213
214
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
218             # '='.
219             if not longopt_re.match(long):
220                 raise DistutilsGetoptError, \
221                       ("invalid long option name '%s' " +
222                        "(must be letters, numbers, hyphens only") % long
223
224             self.attr_name[long] = self.get_attr_name(long)
225             if short:
226                 self.short_opts.append(short)
227                 self.short2long[short[0]] = long
228
229         # for option_table
230
231     # _grok_option_table()
232
233
234     def getopt (self, args=None, object=None):
235         """Parse command-line options in args. Store as attributes on object.
236
237         If 'args' is None or not supplied, uses 'sys.argv[1:]'.  If
238         'object' is None or not supplied, creates a new OptionDummy
239         object, stores option values there, and returns a tuple (args,
240         object).  If 'object' is supplied, it is modified in place and
241         'getopt()' just returns 'args'; in both cases, the returned
242         'args' is a modified copy of the passed-in 'args' list, which
243         is left untouched.
244         """
245         if args is None:
246             args = sys.argv[1:]
247         if object is None:
248             object = OptionDummy()
249             created_object = 1
250         else:
251             created_object = 0
252
253         self._grok_option_table()
254
255         short_opts = string.join(self.short_opts)
256         try:
257             opts, args = getopt.getopt(args, short_opts, self.long_opts)
258         except getopt.error, msg:
259             raise DistutilsArgError, msg
260
261         for opt, val in opts:
262             if len(opt) == 2 and opt[0] == '-': # it's a short option
263                 opt = self.short2long[opt[1]]
264             else:
265                 assert len(opt) > 2 and opt[:2] == '--'
266                 opt = opt[2:]
267
268             alias = self.alias.get(opt)
269             if alias:
270                 opt = alias
271
272             if not self.takes_arg[opt]:     # boolean option?
273                 assert val == '', "boolean option can't have value"
274                 alias = self.negative_alias.get(opt)
275                 if alias:
276                     opt = alias
277                     val = 0
278                 else:
279                     val = 1
280
281             attr = self.attr_name[opt]
282             # The only repeating option at the moment is 'verbose'.
283             # It has a negative option -q quiet, which should set verbose = 0.
284             if val and self.repeat.get(attr) is not None:
285                 val = getattr(object, attr, 0) + 1
286             setattr(object, attr, val)
287             self.option_order.append((opt, val))
288
289         # for opts
290         if created_object:
291             return args, object
292         else:
293             return args
294
295     # getopt()
296
297
298     def get_option_order (self):
299         """Returns the list of (option, value) tuples processed by the
300         previous run of 'getopt()'.  Raises RuntimeError if
301         'getopt()' hasn't been called yet.
302         """
303         if self.option_order is None:
304             raise RuntimeError, "'getopt()' hasn't been called yet"
305         else:
306             return self.option_order
307
308
309     def generate_help (self, header=None):
310         """Generate help text (a list of strings, one per suggested line of
311         output) from the option table for this FancyGetopt object.
312         """
313         # Blithely assume the option table is good: probably wouldn't call
314         # 'generate_help()' unless you've already called 'getopt()'.
315
316         # First pass: determine maximum length of long option names
317         max_opt = 0
318         for option in self.option_table:
319             long = option[0]
320             short = option[1]
321             l = len(long)
322             if long[-1] == '=':
323                 l = l - 1
324             if short is not None:
325                 l = l + 5                   # " (-x)" where short == 'x'
326             if l > max_opt:
327                 max_opt = l
328
329         opt_width = max_opt + 2 + 2 + 2     # room for indent + dashes + gutter
330
331         # Typical help block looks like this:
332         #   --foo       controls foonabulation
333         # Help block for longest option looks like this:
334         #   --flimflam  set the flim-flam level
335         # and with wrapped text:
336         #   --flimflam  set the flim-flam level (must be between
337         #               0 and 100, except on Tuesdays)
338         # Options with short names will have the short name shown (but
339         # it doesn't contribute to max_opt):
340         #   --foo (-f)  controls foonabulation
341         # If adding the short option would make the left column too wide,
342         # we push the explanation off to the next line
343         #   --flimflam (-l)
344         #               set the flim-flam level
345         # Important parameters:
346         #   - 2 spaces before option block start lines
347         #   - 2 dashes for each long option name
348         #   - min. 2 spaces between option and explanation (gutter)
349         #   - 5 characters (incl. space) for short option name
350
351         # Now generate lines of help text.  (If 80 columns were good enough
352         # for Jesus, then 78 columns are good enough for me!)
353         line_width = 78
354         text_width = line_width - opt_width
355         big_indent = ' ' * opt_width
356         if header:
357             lines = [header]
358         else:
359             lines = ['Option summary:']
360
361         for option in self.option_table:
362             long, short, help = option[:3]
363             text = wrap_text(help, text_width)
364             if long[-1] == '=':
365                 long = long[0:-1]
366
367             # Case 1: no short option at all (makes life easy)
368             if short is None:
369                 if text:
370                     lines.append("  --%-*s  %s" % (max_opt, long, text[0]))
371                 else:
372                     lines.append("  --%-*s  " % (max_opt, long))
373
374             # Case 2: we have a short option, so we have to include it
375             # just after the long option
376             else:
377                 opt_names = "%s (-%s)" % (long, short)
378                 if text:
379                     lines.append("  --%-*s  %s" %
380                                  (max_opt, opt_names, text[0]))
381                 else:
382                     lines.append("  --%-*s" % opt_names)
383
384             for l in text[1:]:
385                 lines.append(big_indent + l)
386
387         # for self.option_table
388
389         return lines
390
391     # generate_help ()
392
393     def print_help (self, header=None, file=None):
394         if file is None:
395             file = sys.stdout
396         for line in self.generate_help(header):
397             file.write(line + "\n")
398
399 # class FancyGetopt
400
401
402 def fancy_getopt (options, negative_opt, object, args):
403     parser = FancyGetopt(options)
404     parser.set_negative_aliases(negative_opt)
405     return parser.getopt(args, object)
406
407
408 WS_TRANS = string.maketrans(string.whitespace, ' ' * len(string.whitespace))
409
410 def wrap_text (text, width):
411     """wrap_text(text : string, width : int) -> [string]
412
413     Split 'text' into multiple lines of no more than 'width' characters
414     each, and return the list of strings that results.
415     """
416
417     if text is None:
418         return []
419     if len(text) <= width:
420         return [text]
421
422     text = string.expandtabs(text)
423     text = string.translate(text, WS_TRANS)
424     chunks = re.split(r'( +|-+)', text)
425     chunks = filter(None, chunks)      # ' - ' results in empty strings
426     lines = []
427
428     while chunks:
429
430         cur_line = []                   # list of chunks (to-be-joined)
431         cur_len = 0                     # length of current line
432
433         while chunks:
434             l = len(chunks[0])
435             if cur_len + l <= width:    # can squeeze (at least) this chunk in
436                 cur_line.append(chunks[0])
437                 del chunks[0]
438                 cur_len = cur_len + l
439             else:                       # this line is full
440                 # drop last chunk if all space
441                 if cur_line and cur_line[-1][0] == ' ':
442                     del cur_line[-1]
443                 break
444
445         if chunks:                      # any chunks left to process?
446
447             # if the current line is still empty, then we had a single
448             # chunk that's too big too fit on a line -- so we break
449             # down and break it up at the line width
450             if cur_len == 0:
451                 cur_line.append(chunks[0][0:width])
452                 chunks[0] = chunks[0][width:]
453
454             # all-whitespace chunks at the end of a line can be discarded
455             # (and we know from the re.split above that if a chunk has
456             # *any* whitespace, it is *all* whitespace)
457             if chunks[0][0] == ' ':
458                 del chunks[0]
459
460         # and store this line in the list-of-all-lines -- as a single
461         # string, of course!
462         lines.append(string.join(cur_line, ''))
463
464     # while chunks
465
466     return lines
467
468
469 def translate_longopt(opt):
470     """Convert a long option name to a valid Python identifier by
471     changing "-" to "_".
472     """
473     return string.translate(opt, longopt_xlate)
474
475
476 class OptionDummy:
477     """Dummy class just used as a place to hold command-line option
478     values as instance attributes."""
479
480     def __init__ (self, options=[]):
481         """Create a new OptionDummy instance.  The attributes listed in
482         'options' will be initialized to None."""
483         for opt in options:
484             setattr(self, opt, None)