]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/distutils/fancy_getopt.py
getpass
[plan9front.git] / sys / lib / python / 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 # This module should be kept compatible with Python 2.1.
12
13 __revision__ = "$Id: fancy_getopt.py 37828 2004-11-10 22:23:15Z loewis $"
14
15 import sys, string, re
16 from types import *
17 import getopt
18 from 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...
24 longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
25 longopt_re = re.compile(r'^%s$' % longopt_pat)
26
27 # For recognizing "negative alias" options, eg. "quiet=!verbose"
28 neg_alias_re = re.compile("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))
29
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('-', '_')
33
34 class FancyGetopt:
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
39         from them
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
44     """
45
46     def __init__ (self, option_table=None):
47
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
57
58         # 'option_index' maps long option names to entries in the option
59         # table (ie. those 3-tuples).
60         self.option_index = {}
61         if self.option_table:
62             self._build_index()
63
64         # 'alias' records (duh) alias options; {'foo': 'bar'} means
65         # --foo is an alias for --bar
66         self.alias = {}
67
68         # 'negative_alias' keeps track of options that are the boolean
69         # opposite of some other option
70         self.negative_alias = {}
71
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.
76         self.short_opts = []
77         self.long_opts = []
78         self.short2long = {}
79         self.attr_name = {}
80         self.takes_arg = {}
81
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 = []
86
87     # __init__ ()
88
89
90     def _build_index (self):
91         self.option_index.clear()
92         for option in self.option_table:
93             self.option_index[option[0]] = option
94
95     def set_option_table (self, option_table):
96         self.option_table = option_table
97         self._build_index()
98
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
103         else:
104             option = (long_option, short_option, help_string)
105             self.option_table.append(option)
106             self.option_index[long_option] = option
107
108
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)
113
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
117         to underscores."""
118         return string.translate(long_option, longopt_xlate)
119
120
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, \
126                       ("invalid %s '%s': "
127                        "option '%s' not defined") % (what, alias, alias)
128             if not self.option_index.has_key(opt):
129                 raise DistutilsGetoptError, \
130                       ("invalid %s '%s': "
131                        "aliased option '%s' not defined") % (what, alias, opt)
132
133     def set_aliases (self, alias):
134         """Set the aliases for this option parser."""
135         self._check_alias_dict(alias, "alias")
136         self.alias = alias
137
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
145
146
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
150         worthwhile.
151         """
152         self.long_opts = []
153         self.short_opts = []
154         self.short2long.clear()
155         self.repeat = {}
156
157         for option in self.option_table:
158             if len(option) == 3:
159                 long, short, help = option
160                 repeat = 0
161             elif len(option) == 4:
162                 long, short, help, repeat = option
163             else:
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,)
167
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
173
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
179
180             self.repeat[long] = repeat
181             self.long_opts.append(long)
182
183             if long[-1] == '=':             # option takes an argument?
184                 if short: short = short + ':'
185                 long = long[0:-1]
186                 self.takes_arg[long] = 1
187             else:
188
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") % \
197                                (long, alias_to)
198
199                     self.long_opts[-1] = long # XXX redundant?!
200                     self.takes_arg[long] = 0
201
202                 else:
203                     self.takes_arg[long] = 0
204
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)
214
215
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
219             # '='.
220             if not longopt_re.match(long):
221                 raise DistutilsGetoptError, \
222                       ("invalid long option name '%s' " +
223                        "(must be letters, numbers, hyphens only") % long
224
225             self.attr_name[long] = self.get_attr_name(long)
226             if short:
227                 self.short_opts.append(short)
228                 self.short2long[short[0]] = long
229
230         # for option_table
231
232     # _grok_option_table()
233
234
235     def getopt (self, args=None, object=None):
236         """Parse command-line options in args. Store as attributes on object.
237
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
244         is left untouched.
245         """
246         if args is None:
247             args = sys.argv[1:]
248         if object is None:
249             object = OptionDummy()
250             created_object = 1
251         else:
252             created_object = 0
253
254         self._grok_option_table()
255
256         short_opts = string.join(self.short_opts)
257         try:
258             opts, args = getopt.getopt(args, short_opts, self.long_opts)
259         except getopt.error, msg:
260             raise DistutilsArgError, msg
261
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]]
265             else:
266                 assert len(opt) > 2 and opt[:2] == '--'
267                 opt = opt[2:]
268
269             alias = self.alias.get(opt)
270             if alias:
271                 opt = alias
272
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)
276                 if alias:
277                     opt = alias
278                     val = 0
279                 else:
280                     val = 1
281
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))
289
290         # for opts
291         if created_object:
292             return args, object
293         else:
294             return args
295
296     # getopt()
297
298
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.
303         """
304         if self.option_order is None:
305             raise RuntimeError, "'getopt()' hasn't been called yet"
306         else:
307             return self.option_order
308
309
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.
313         """
314         # Blithely assume the option table is good: probably wouldn't call
315         # 'generate_help()' unless you've already called 'getopt()'.
316
317         # First pass: determine maximum length of long option names
318         max_opt = 0
319         for option in self.option_table:
320             long = option[0]
321             short = option[1]
322             l = len(long)
323             if long[-1] == '=':
324                 l = l - 1
325             if short is not None:
326                 l = l + 5                   # " (-x)" where short == 'x'
327             if l > max_opt:
328                 max_opt = l
329
330         opt_width = max_opt + 2 + 2 + 2     # room for indent + dashes + gutter
331
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
344         #   --flimflam (-l)
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
351
352         # Now generate lines of help text.  (If 80 columns were good enough
353         # for Jesus, then 78 columns are good enough for me!)
354         line_width = 78
355         text_width = line_width - opt_width
356         big_indent = ' ' * opt_width
357         if header:
358             lines = [header]
359         else:
360             lines = ['Option summary:']
361
362         for option in self.option_table:
363             long, short, help = option[:3]
364             text = wrap_text(help, text_width)
365             if long[-1] == '=':
366                 long = long[0:-1]
367
368             # Case 1: no short option at all (makes life easy)
369             if short is None:
370                 if text:
371                     lines.append("  --%-*s  %s" % (max_opt, long, text[0]))
372                 else:
373                     lines.append("  --%-*s  " % (max_opt, long))
374
375             # Case 2: we have a short option, so we have to include it
376             # just after the long option
377             else:
378                 opt_names = "%s (-%s)" % (long, short)
379                 if text:
380                     lines.append("  --%-*s  %s" %
381                                  (max_opt, opt_names, text[0]))
382                 else:
383                     lines.append("  --%-*s" % opt_names)
384
385             for l in text[1:]:
386                 lines.append(big_indent + l)
387
388         # for self.option_table
389
390         return lines
391
392     # generate_help ()
393
394     def print_help (self, header=None, file=None):
395         if file is None:
396             file = sys.stdout
397         for line in self.generate_help(header):
398             file.write(line + "\n")
399
400 # class FancyGetopt
401
402
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)
407
408
409 WS_TRANS = string.maketrans(string.whitespace, ' ' * len(string.whitespace))
410
411 def wrap_text (text, width):
412     """wrap_text(text : string, width : int) -> [string]
413
414     Split 'text' into multiple lines of no more than 'width' characters
415     each, and return the list of strings that results.
416     """
417
418     if text is None:
419         return []
420     if len(text) <= width:
421         return [text]
422
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
427     lines = []
428
429     while chunks:
430
431         cur_line = []                   # list of chunks (to-be-joined)
432         cur_len = 0                     # length of current line
433
434         while chunks:
435             l = len(chunks[0])
436             if cur_len + l <= width:    # can squeeze (at least) this chunk in
437                 cur_line.append(chunks[0])
438                 del 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] == ' ':
443                     del cur_line[-1]
444                 break
445
446         if chunks:                      # any chunks left to process?
447
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
451             if cur_len == 0:
452                 cur_line.append(chunks[0][0:width])
453                 chunks[0] = chunks[0][width:]
454
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] == ' ':
459                 del chunks[0]
460
461         # and store this line in the list-of-all-lines -- as a single
462         # string, of course!
463         lines.append(string.join(cur_line, ''))
464
465     # while chunks
466
467     return lines
468
469 # wrap_text ()
470
471
472 def translate_longopt (opt):
473     """Convert a long option name to a valid Python identifier by
474     changing "-" to "_".
475     """
476     return string.translate(opt, longopt_xlate)
477
478
479 class OptionDummy:
480     """Dummy class just used as a place to hold command-line option
481     values as instance attributes."""
482
483     def __init__ (self, options=[]):
484         """Create a new OptionDummy instance.  The attributes listed in
485         'options' will be initialized to None."""
486         for opt in options:
487             setattr(self, opt, None)
488
489 # class OptionDummy
490
491
492 if __name__ == "__main__":
493     text = """\
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?"].)"""
498
499     for w in (10, 20, 30, 40):
500         print "width: %d" % w
501         print string.join(wrap_text(text, w), "\n")
502         print