summaryrefslogtreecommitdiffstats
path: root/Lib/distutils/fancy_getopt.py
blob: 3110ab30dba812c068fc0ae14bae98339e882cb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
"""distutils.fancy_getopt

Wrapper around the standard getopt module that provides the following
additional features:
  * short and long options are tied together
  * options have help strings, so fancy_getopt could potentially
    create a complete usage summary
  * options set attributes of a passed-in object
"""

# created 1999/03/03, Greg Ward

__revision__ = "$Id$"

import sys, string, re
from types import *
import getopt
from distutils.errors import *

# Much like command_re in distutils.core, this is close to but not quite
# the same as a Python NAME -- except, in the spirit of most GNU
# utilities, we use '-' in place of '_'.  (The spirit of LISP lives on!)
# The similarities to NAME are again not a coincidence...
longopt_pat = r'[a-zA-Z](?:[a-zA-Z0-9-]*)'
longopt_re = re.compile (r'^%s$' % longopt_pat)

# For recognizing "negative alias" options, eg. "quiet=!verbose"
neg_alias_re = re.compile ("^(%s)=!(%s)$" % (longopt_pat, longopt_pat))


# This is used to translate long options to legitimate Python identifiers
# (for use as attributes of some object).
longopt_xlate = string.maketrans ('-', '_')


def fancy_getopt (options, negative_opt, object, args):

    # The 'options' table is a list of 3-tuples:
    #   (long_option, short_option, help_string)
    # if an option takes an argument, its long_option should have '='
    # appended; short_option should just be a single character, no ':' in
    # any case.  If a long_option doesn't have a corresponding
    # short_option, short_option should be None.  All option tuples must
    # have long options.

    # Build the short_opts string and long_opts list, remembering how
    # the two are tied together

    short_opts = []                     # we'll join 'em when done
    long_opts = []
    short2long = {}
    attr_name = {}
    takes_arg = {}

    for option in options:
        try:
            (long, short, help) = option
        except ValueError:
            raise DistutilsGetoptError, \
                  "invalid option tuple " + str (option)

        # Type-check the option names
        if type (long) is not StringType or len (long) < 2:
            raise DistutilsGetoptError, \
                  "long option '%s' must be a string of length >= 2" % \
                  long

        if (not ((short is None) or
                 (type (short) is StringType and len (short) == 1))):
            raise DistutilsGetoptError, \
                  "short option '%s' must be None or string of length 1" % \
                  short

        long_opts.append (long)

        if long[-1] == '=':             # option takes an argument?
            if short: short = short + ':'
            long = long[0:-1]
            takes_arg[long] = 1
        else:

            # Is option is a "negative alias" for some other option (eg.
            # "quiet" == "!verbose")?
            alias_to = negative_opt.get(long)
            if alias_to is not None:
                if not takes_arg.has_key(alias_to) or takes_arg[alias_to]:
                    raise DistutilsGetoptError, \
                          ("option '%s' is a negative alias for '%s', " +
                           "which either hasn't been defined yet " +
                           "or takes an argument") % (long, alias_to)

                long_opts[-1] = long
                takes_arg[long] = 0

            else:
                takes_arg[long] = 0
                

        # Now enforce some bondage on the long option name, so we can later
        # translate it to an attribute name in 'object'.  Have to do this a
        # bit late to make sure we've removed any trailing '='.
        if not longopt_re.match (long):
            raise DistutilsGetoptError, \
                  ("invalid long option name '%s' " +
                   "(must be letters, numbers, hyphens only") % long

        attr_name[long] = string.translate (long, longopt_xlate)
        if short:
            short_opts.append (short)
            short2long[short[0]] = long

    # end loop over 'options'

    short_opts = string.join (short_opts)
    try:
        (opts, args) = getopt.getopt (args, short_opts, long_opts)
    except getopt.error, msg:
        raise DistutilsArgError, msg

    for (opt, val) in opts:
        if len (opt) == 2 and opt[0] == '-': # it's a short option
            opt = short2long[opt[1]]

        elif len (opt) > 2 and opt[0:2] == '--':
            opt = opt[2:]

        else:
            raise RuntimeError, "getopt lies! (bad option string '%s')" % \
                  opt

        attr = attr_name[opt]
        if takes_arg[opt]:
            setattr (object, attr, val)
        else:
            if val == '':
                alias = negative_opt.get (opt)
                if alias:
                    setattr (object, attr_name[alias], 0)
                else:
                    setattr (object, attr, 1)
            else:
                raise RuntimeError, "getopt lies! (bad value '%s')" % value

    # end loop over options found in 'args'

    return args

# fancy_getopt()


WS_TRANS = string.maketrans (string.whitespace, ' ' * len (string.whitespace))

def wrap_text (text, width):

    if text is None:
        return []
    if len (text) <= width:
        return [text]

    text = string.expandtabs (text)
    text = string.translate (text, WS_TRANS)
    chunks = re.split (r'( +|-+)', text)
    chunks = filter (None, chunks)      # ' - ' results in empty strings
    lines = []

    while chunks:

        cur_line = []                   # list of chunks (to-be-joined)
        cur_len = 0                     # length of current line

        while chunks:
            l = len (chunks[0])
            if cur_len + l <= width:    # can squeeze (at least) this chunk in
                cur_line.append (chunks[0])
                del chunks[0]
                cur_len = cur_len + l
            else:                       # this line is full
                # drop last chunk if all space
                if cur_line and cur_line[-1][0] == ' ':
                    del cur_line[-1]
                break

        if chunks:                      # any chunks left to process?

            # if the current line is still empty, then we had a single
            # chunk that's too big too fit on a line -- so we break
            # down and break it up at the line width
            if cur_len == 0:
                cur_line.append (chunks[0][0:width])
                chunks[0] = chunks[0][width:]

            # all-whitespace chunks at the end of a line can be discarded
            # (and we know from the re.split above that if a chunk has
            # *any* whitespace, it is *all* whitespace)
            if chunks[0][0] == ' ':
                del chunks[0]

        # and store this line in the list-of-all-lines -- as a single
        # string, of course!
        lines.append (string.join (cur_line, ''))

    # while chunks

    return lines

# wrap_text ()
        

def generate_help (options, header=None):
    """Generate help text (a list of strings, one per suggested line of
       output) from an option table."""

    # Blithely assume the option table is good: probably wouldn't call
    # 'generate_help()' unless you've already called 'fancy_getopt()'.

    # First pass: determine maximum length of long option names
    max_opt = 0
    for option in options:
        long = option[0]
        short = option[1]
        l = len (long)
        if long[-1] == '=':
            l = l - 1
        if short is not None:
            l = l + 5                   # " (-x)" where short == 'x'
        if l > max_opt:
            max_opt = l
            
    opt_width = max_opt + 2 + 2 + 2     # room for indent + dashes + gutter

    # Typical help block looks like this:
    #   --foo       controls foonabulation
    # Help block for longest option looks like this:
    #   --flimflam  set the flim-flam level
    # and with wrapped text:
    #   --flimflam  set the flim-flam level (must be between
    #               0 and 100, except on Tuesdays)
    # Options with short names will have the short name shown (but
    # it doesn't contribute to max_opt):
    #   --foo (-f)  controls foonabulation
    # If adding the short option would make the left column too wide,
    # we push the explanation off to the next line
    #   --flimflam (-l)
    #               set the flim-flam level
    # Important parameters:
    #   - 2 spaces before option block start lines
    #   - 2 dashes for each long option name
    #   - min. 2 spaces between option and explanation (gutter)
    #   - 5 characters (incl. space) for short option name

    # Now generate lines of help text.
    line_width = 78                     # if 80 columns were good enough for
    text_width = line_width - opt_width # Jesus, then 78 are good enough for me
    big_indent = ' ' * opt_width
    if header:
        lines = [header]
    else:
        lines = ['Option summary:']

    for (long,short,help) in options:
       
        text = wrap_text (help, text_width)
        if long[-1] == '=':
            long = long[0:-1]

        # Case 1: no short option at all (makes life easy)
        if short is None:
            if text:
                lines.append ("  --%-*s  %s" % (max_opt, long, text[0]))
            else:
                lines.append ("  --%-*s  " % (max_opt, long))

            for l in text[1:]:
                lines.append (big_indent + l)

        # Case 2: we have a short option, so we have to include it
        # just after the long option
        else:
            opt_names = "%s (-%s)" % (long, short)
            if text:
                lines.append ("  --%-*s  %s" %
                              (max_opt, opt_names, text[0]))
            else:
                lines.append ("  --%-*s" % opt_names)

    # for loop over options

    return lines

# generate_help ()


def print_help (options, file=None, header=None):
    if file is None:
        file = sys.stdout
    for line in generate_help (options, header):
        file.write (line + "\n")
# print_help ()
    

if __name__ == "__main__":
    text = """\
Tra-la-la, supercalifragilisticexpialidocious.
How *do* you spell that odd word, anyways?
(Someone ask Mary -- she'll know [or she'll
say, "How should I know?"].)"""

    for w in (10, 20, 30, 40):
        print "width: %d" % w
        print string.join (wrap_text (text, w), "\n")
        print