PageRenderTime 719ms CodeModel.GetById 223ms app.highlight 225ms RepoModel.GetById 180ms app.codeStats 1ms

/Lib/distutils/fancy_getopt.py

http://unladen-swallow.googlecode.com/
Python | 502 lines | 375 code | 48 blank | 79 comment | 35 complexity | 223416e3cd663965c347b7ab8e3484de MD5 | raw file
  1"""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# This module should be kept compatible with Python 2.1.
 12
 13__revision__ = "$Id: fancy_getopt.py 60923 2008-02-21 18:18:37Z guido.van.rossum $"
 14
 15import sys, string, re
 16from 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...
 24longopt_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
 30# This is used to translate long options to legitimate Python identifiers
 31# (for use as attributes of some object).
 32longopt_xlate = string.maketrans('-', '_')
 33
 34class 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 long_option in self.option_index:
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 long_option in self.option_index
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 alias not in self.option_index:
125                raise DistutilsGetoptError, \
126                      ("invalid %s '%s': "
127                       "option '%s' not defined") % (what, alias, alias)
128            if opt not in self.option_index:
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
403def 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
409WS_TRANS = string.maketrans(string.whitespace, ' ' * len(string.whitespace))
410
411def 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
472def 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
479class 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
492if __name__ == "__main__":
493    text = """\
494Tra-la-la, supercalifragilisticexpialidocious.
495How *do* you spell that odd word, anyways?
496(Someone ask Mary -- she'll know [or she'll
497say, "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