summaryrefslogtreecommitdiffstats
path: root/cobbler/Cheetah/Utils/optik/option_parser.py
blob: 1b4e6328038555d72ab76ced49ed732307db4e11 (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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
"""optik.option_parser

Provides the OptionParser and Values classes.

Cheetah modifications:  added "Cheetah.Utils.optik." prefix to
  all intra-Optik imports.
"""

__revision__ = "$Id: option_parser.py,v 1.2 2002/09/12 06:56:51 hierro Exp $"

# Copyright (c) 2001 Gregory P. Ward.  All rights reserved.
# See the README.txt distributed with Optik for licensing terms.

# created 2001/10/17, GPW (from optik.py)

import sys, os
import types
from Cheetah.Utils.optik.option import Option, NO_DEFAULT
from Cheetah.Utils.optik.errors import OptionConflictError, OptionValueError, BadOptionError

def get_prog_name ():
    return os.path.basename(sys.argv[0])


SUPPRESS_HELP = "SUPPRESS"+"HELP"
SUPPRESS_USAGE = "SUPPRESS"+"USAGE"

STD_HELP_OPTION = Option("-h", "--help",
                         action="help",
                         help="show this help message and exit")
STD_VERSION_OPTION = Option("--version",
                            action="version",
                            help="show program's version number and exit")


class Values:

    def __init__ (self, defaults=None):
        if defaults:
            for (attr, val) in defaults.items():
                setattr(self, attr, val)


    def _update_careful (self, dict):
        """
        Update the option values from an arbitrary dictionary, but only
        use keys from dict that already have a corresponding attribute
        in self.  Any keys in dict without a corresponding attribute
        are silently ignored.
        """
        for attr in dir(self):
            if dict.has_key(attr):
                dval = dict[attr]
                if dval is not None:
                    setattr(self, attr, dval)

    def _update_loose (self, dict):
        """
        Update the option values from an arbitrary dictionary,
        using all keys from the dictionary regardless of whether
        they have a corresponding attribute in self or not.
        """
        self.__dict__.update(dict)

    def _update (self, dict, mode):
        if mode == "careful":
            self._update_careful(dict)
        elif mode == "loose":
            self._update_loose(dict)
        else:
            raise ValueError, "invalid update mode: %r" % mode

    def read_module (self, modname, mode="careful"):
        __import__(modname)
        mod = sys.modules[modname]
        self._update(vars(mod), mode)

    def read_file (self, filename, mode="careful"):
        vars = {}
        execfile(filename, vars)
        self._update(vars, mode)

    def ensure_value (self, attr, value):
        if not hasattr(self, attr) or getattr(self, attr) is None:
            setattr(self, attr, value)
        return getattr(self, attr)


class OptionParser:
    """
    Class attributes:
      standard_option_list : [Option]
        list of standard options that will be accepted by all instances
        of this parser class (intended to be overridden by subclasses).

    Instance attributes:
      usage : string
        a usage string for your program.  Before it is displayed
        to the user, "%prog" will be expanded to the name of
        your program (os.path.basename(sys.argv[0])).
      option_list : [Option]
        the list of all options accepted on the command-line of
        this program
      _short_opt : { string : Option }
        dictionary mapping short option strings, eg. "-f" or "-X",
        to the Option instances that implement them.  If an Option
        has multiple short option strings, it will appears in this
        dictionary multiple times.
      _long_opt : { string : Option }
        dictionary mapping long option strings, eg. "--file" or
        "--exclude", to the Option instances that implement them.
        Again, a given Option can occur multiple times in this
        dictionary.
      _long_opts : [string]
        list of long option strings recognized by this option
        parser.  Should be equal to _long_opt.values().
      defaults : { string : any }
        dictionary mapping option destination names to default
        values for each destination.

      allow_interspersed_args : boolean = true
        if true, positional arguments may be interspersed with options.
        Assuming -a and -b each take a single argument, the command-line
          -ablah foo bar -bboo baz
        will be interpreted the same as
          -ablah -bboo -- foo bar baz
        If this flag were false, that command line would be interpreted as
          -ablah -- foo bar -bboo baz
        -- ie. we stop processing options as soon as we see the first
        non-option argument.  (This is the tradition followed by
        Python's getopt module, Perl's Getopt::Std, and other argument-
        parsing libraries, but it is generally annoying to users.)

      rargs : [string]
        the argument list currently being parsed.  Only set when
        parse_args() is active, and continually trimmed down as
        we consume arguments.  Mainly there for the benefit of
        callback options.
      largs : [string]
        the list of leftover arguments that we have skipped while
        parsing options.  If allow_interspersed_args is false, this
        list is always empty.
      values : Values
        the set of option values currently being accumulated.  Only
        set when parse_args() is active.  Also mainly for callbacks.

    Because of the 'rargs', 'largs', and 'values' attributes,
    OptionParser is not thread-safe.  If, for some perverse reason, you
    need to parse command-line arguments simultaneously in different
    threads, use different OptionParser instances.
    
    """ 

    standard_option_list = [STD_HELP_OPTION]


    def __init__ (self,
                  usage=None,
                  option_list=None,
                  option_class=Option,
                  version=None,
                  conflict_handler="error"):
        self.set_usage(usage)
        self.option_class = option_class
        self.version = version
        self.set_conflict_handler(conflict_handler)
        self.allow_interspersed_args = 1

        # Create the various lists and dicts that constitute the
        # "option list".  See class docstring for details about
        # each attribute.
        self._create_option_list()

        # Populate the option list; initial sources are the
        # standard_option_list class attribute, the 'option_list'
        # argument, and the STD_VERSION_OPTION global (if 'version'
        # supplied).
        self._populate_option_list(option_list)

        self._init_parsing_state()

    # -- Private methods -----------------------------------------------
    # (used by the constructor)

    def _create_option_list (self):
        self.option_list = []
        self._short_opt = {}            # single letter -> Option instance
        self._long_opt = {}             # long option -> Option instance
        self._long_opts = []            # list of long options
        self.defaults = {}              # maps option dest -> default value

    def _populate_option_list (self, option_list):
        if self.standard_option_list:
            self.add_options(self.standard_option_list)
        if self.version:
            self.add_option(STD_VERSION_OPTION)
        if option_list:
            self.add_options(option_list)
        
    def _init_parsing_state (self):
        # These are set in parse_args() for the convenience of callbacks.
        self.rargs = None
        self.largs = None
        self.values = None


    # -- Simple modifier methods ---------------------------------------

    def set_usage (self, usage):
        if usage is None:
            self.usage = "usage: %prog [options]"
        elif usage is SUPPRESS_USAGE:
            self.usage = None
        else:
            self.usage = usage

    def enable_interspersed_args (self):
        self.allow_interspersed_args = 1

    def disable_interspersed_args (self):
        self.allow_interspersed_args = 0

    def set_conflict_handler (self, handler):
        if handler not in ("ignore", "error", "resolve"):
            raise ValueError, "invalid conflict_resolution value %r" % handler
        self.conflict_handler = handler

    def set_default (self, dest, value):
        self.defaults[dest] = value

    def set_defaults (self, **kwargs):
        self.defaults.update(kwargs)


    # -- Option-adding methods -----------------------------------------

    def _check_conflict (self, option):
        conflict_opts = []
        for opt in option._short_opts:
            if self._short_opt.has_key(opt):
                conflict_opts.append((opt, self._short_opt[opt]))
        for opt in option._long_opts:
            if self._long_opt.has_key(opt):
                conflict_opts.append((opt, self._long_opt[opt]))

        if conflict_opts:
            handler = self.conflict_handler
            if handler == "ignore":     # behaviour for Optik 1.0, 1.1
                pass
            elif handler == "error":    # new in 1.2
                raise OptionConflictError(
                    "conflicting option string(s): %s"
                    % ", ".join([co[0] for co in conflict_opts]),
                    option)
            elif handler == "resolve":  # new in 1.2
                for (opt, c_option) in conflict_opts:
                    if opt.startswith("--"):
                        c_option._long_opts.remove(opt)
                        del self._long_opt[opt]
                    else:
                        c_option._short_opts.remove(opt)
                        del self._short_opt[opt]
                    if not (c_option._short_opts or c_option._long_opts):
                        self.option_list.remove(c_option)


    def add_option (self, *args, **kwargs):
        """add_option(Option)
           add_option(opt_str, ..., kwarg=val, ...)
        """
        if type(args[0]) is types.StringType:
            option = self.option_class(*args, **kwargs)
        elif len(args) == 1 and not kwargs:
            option = args[0]
            if not isinstance(option, Option):
                raise TypeError, "not an Option instance: %r" % option
        else:
            raise TypeError, "invalid arguments"

        self._check_conflict(option)

        self.option_list.append(option)
        for opt in option._short_opts:
            self._short_opt[opt] = option
        for opt in option._long_opts:
            self._long_opt[opt] = option
            self._long_opts.append(opt)

        if option.dest is not None:     # option has a dest, we need a default
            if option.default is not NO_DEFAULT:
                self.defaults[option.dest] = option.default
            elif not self.defaults.has_key(option.dest):
                self.defaults[option.dest] = None

    def add_options (self, option_list):
        for option in option_list:
            self.add_option(option)


    # -- Option query/removal methods ----------------------------------

    def get_option (self, opt_str):
        return (self._short_opt.get(opt_str) or
                self._long_opt.get(opt_str))

    def has_option (self, opt_str):
        return (self._short_opt.has_key(opt_str) or
                self._long_opt.has_key(opt_str))


    def remove_option (self, opt_str):
        option = self._short_opt.get(opt_str)
        if option is None:
            option = self._long_opt.get(opt_str)
        if option is None:
            raise ValueError("no such option %r" % opt_str)

        for opt in option._short_opts:
            del self._short_opt[opt]
        for opt in option._long_opts:
            del self._long_opt[opt]
            self._long_opts.remove(opt)
        self.option_list.remove(option)


    # -- Option-parsing methods ----------------------------------------

    def _get_args (self, args):
        if args is None:
            return sys.argv[1:]
        else:
            return args[:]              # don't modify caller's list

    def parse_args (self, args=None, values=None):
        """
        parse_args(args : [string] = sys.argv[1:],
                   values : Values = None)
        -> (values : Values, args : [string])

        Parse the command-line options found in 'args' (default:
        sys.argv[1:]).  Any errors result in a call to 'error()', which
        by default prints the usage message to stderr and calls
        sys.exit() with an error message.  On success returns a pair
        (values, args) where 'values' is an Values instance (with all
        your option values) and 'args' is the list of arguments left
        over after parsing options.
        """
        rargs = self._get_args(args)
        if values is None:
            values = Values(self.defaults)

        # Store the halves of the argument list as attributes for the
        # convenience of callbacks:
        #   rargs
        #     the rest of the command-line (the "r" stands for
        #     "remaining" or "right-hand")
        #   largs
        #     the leftover arguments -- ie. what's left after removing
        #     options and their arguments (the "l" stands for "leftover"
        #     or "left-hand")

        # Say this is the original argument list:
        # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)]
        #                            ^
        # (we are about to process arg(i)).
        #
        # Then rargs is [arg(i), ..., arg(N-1)]
        # and largs is a *subset* of [arg0, ..., arg(i-1)]
        # (any options and their arguments will have been removed
        # from largs).
        #
        # _process_arg() will always consume 1 or more arguments.
        # If it consumes 1 (eg. arg is an option that takes no arguments),
        # then after _process_arg() is done the situation is:
        #   largs = subset of [arg0, ..., arg(i)]
        #   rargs = [arg(i+1), ..., arg(N-1)]
        #
        # If allow_interspersed_args is false, largs will always be
        # *empty* -- still a subset of [arg0, ..., arg(i-1)], but
        # not a very interesting subset!

        self.rargs = rargs
        self.largs = largs = []
        self.values = values

        stop = 0
        while rargs and not stop:
            try:
                stop = self._process_arg(largs, rargs, values)
            except (BadOptionError, OptionValueError), err:
                self.error(err.msg)

        args = largs + rargs
        return self.check_values(values, args)

    def check_values (self, values, args):
        """
        check_values(values : Values, args : [string])
        -> (values : Values, args : [string])

        Check that the supplied option values and leftover arguments are
        valid.  Returns the option values and leftover arguments
        (possibly adjusted, possibly completely new -- whatever you
        like).  Default implementation just returns the passed-in
        values; subclasses may override as desired.
        """
        return (values, args)

    def _process_arg (self, largs, rargs, values):
        """_process_args(largs : [string],
                         rargs : [string],
                         values : Values)
           -> stop : boolean

        Process a single command-line argument, consuming zero or more
        arguments.  The next argument to process is rargs[0], which will
        almost certainly be consumed from rargs.  (It might wind up in
        largs, or it might affect a value in values, or -- if a callback
        is involved -- almost anything might happen.  It will not be
        consumed if it is a non-option argument and
        allow_interspersed_args is false.)  More arguments from rargs
        may also be consumed, depending on circumstances.

        Returns true if option processing should stop after this
        argument is processed.
        """

        # We handle bare "--" explicitly, and bare "-" is handled by the
        # standard arg handler since the short arg case ensures that the len
        # of the opt string is greater than 1.

        arg = rargs[0]
        if arg == "--":
            del rargs[0]
            return 1
        elif arg[0:2] == "--":
            # process a single long option (possibly with value(s))
            self._process_long_opt(rargs, values)
        elif arg[:1] == "-" and len(arg) > 1:
            # process a cluster of short options (possibly with
            # value(s) for the last one only)
            self._process_short_opts(rargs, values)
        else:
            if self.allow_interspersed_args:
                largs.append(arg)
                del rargs[0]
            else:
                return 1                # stop now, leave this arg in rargs

        return 0                        # keep processing args
        
    def _match_long_opt (self, opt):
        """_match_long_opt(opt : string) -> string

        Determine which long option string 'opt' matches, ie. which one
        it is an unambiguous abbrevation for.  Raises BadOptionError if
        'opt' doesn't unambiguously match any long option string.
        """
        return _match_abbrev(opt, self._long_opts)

    def _process_long_opt (self, rargs, values):
        arg = rargs.pop(0)

        # Value explicitly attached to arg?  Pretend it's the next
        # argument.
        if "=" in arg:
            (opt, next_arg) = arg.split("=", 1)
            rargs.insert(0, next_arg)
            had_explicit_value = 1
        else:
            opt = arg
            had_explicit_value = 0

        opt = self._match_long_opt(opt)
        option = self._long_opt[opt]
        if option.takes_value():
            nargs = option.nargs
            if len(rargs) < nargs:
                if nargs == 1:
                    self.error("%s option requires a value" % opt)
                else:
                    self.error("%s option requires %d values"
                               % (opt, nargs))
            elif nargs == 1:
                value = rargs.pop(0)
            else:
                value = tuple(rargs[0:nargs])
                del rargs[0:nargs]

        elif had_explicit_value:
            self.error("%s option does not take a value" % opt)

        else:
            value = None

        option.process(opt, value, values, self)

    def _process_short_opts (self, rargs, values):
        arg = rargs.pop(0)
        stop = 0
        i = 1
        for ch in arg[1:]:
            opt = "-" + ch
            option = self._short_opt.get(opt)
            i += 1                      # we have consumed a character

            if not option:
                self.error("no such option: %s" % opt)
            if option.takes_value():
                # Any characters left in arg?  Pretend they're the
                # next arg, and stop consuming characters of arg.
                if i < len(arg):
                    rargs.insert(0, arg[i:])
                    stop = 1

                nargs = option.nargs
                if len(rargs) < nargs:
                    if nargs == 1:
                        self.error("%s option requires a value" % opt)
                    else:
                        self.error("%s option requires %s values"
                                   % (opt, nargs))
                elif nargs == 1:
                    value = rargs.pop(0)
                else:
                    value = tuple(rargs[0:nargs])
                    del rargs[0:nargs]

            else:                       # option doesn't take a value
                value = None

            option.process(opt, value, values, self)

            if stop:
                break


    # -- Output/error methods ------------------------------------------

    def error (self, msg):
        self.print_usage(sys.stderr)
        sys.exit("%s: error: %s" % (get_prog_name(), msg))

    def print_usage (self, file=None):
        if self.usage:
            usage = self.usage.replace("%prog", get_prog_name())
            print >>file, usage
            print >>file

    def print_version (self, file=None):
        if self.version:
            version = self.version.replace("%prog", get_prog_name())
            print >>file, version

    def print_help (self, file=None):
        from distutils.fancy_getopt import wrap_text
        
        if file is None:
            file = sys.stdout

        self.print_usage(file)

        # The help for each option consists of two parts:
        #   * the opt strings and metavars
        #     eg. ("-x", or "-fFILENAME, --file=FILENAME")
        #   * the user-supplied help string
        #     eg. ("turn on expert mode", "read data from FILENAME")
        #
        # If possible, we write both of these on the same line:
        #   -x      turn on expert mode
        # 
        # But if the opt string list is too long, we put the help
        # string on a second line, indented to the same column it would
        # start in if it fit on the first line.
        #   -fFILENAME, --file=FILENAME
        #           read data from FILENAME

        print >>file, "options:"
        width = 78                      # assume 80 cols for now

        option_help = []                # list of (string, string) tuples
        lengths = []

        for option in self.option_list:
            takes_value = option.takes_value()
            if takes_value:
                metavar = option.metavar or option.dest.upper()

            opts = []               # list of "-a" or "--foo=FILE" strings
            if option.help is SUPPRESS_HELP:
                continue

            if takes_value:
                for sopt in option._short_opts:
                    opts.append(sopt + metavar)
                for lopt in option._long_opts:
                    opts.append(lopt + "=" + metavar)
            else:
                for opt in option._short_opts + option._long_opts:
                    opts.append(opt)

            opts = ", ".join(opts)
            option_help.append((opts, option.help))
            lengths.append(len(opts))

        max_opts = min(max(lengths), 20)

        for (opts, help) in option_help:
            # how much to indent lines 2 .. N of help text
            indent_rest = 2 + max_opts + 2 
            help_width = width - indent_rest

            if len(opts) > max_opts:
                opts = "  " + opts + "\n"
                indent_first = indent_rest

            else:                       # start help on same line as opts
                opts = "  %-*s  " % (max_opts, opts)
                indent_first = 0

            file.write(opts)

            if help:
                help_lines = wrap_text(help, help_width)
                print >>file, "%*s%s" % (indent_first, "", help_lines[0])
                for line in help_lines[1:]:
                    print >>file, "%*s%s" % (indent_rest, "", line)
            elif opts[-1] != "\n":
                file.write("\n")

# class OptionParser


def _match_abbrev (s, words):
    """_match_abbrev(s : string, words : [string]) -> string

    Returns the string in 'words' for which 's' is an unambiguous
    abbreviation.  If 's' is found to be ambiguous or doesn't match any
    of 'words', raises BadOptionError.
    """ 
    match = None
    for word in words:
        # If s isn't even a prefix for this word, don't waste any
        # more time on it: skip to the next word and try again.
        if not word.startswith(s):
            continue

        # Exact match?  Great, return now.
        if s == word:
            return word

        # Now comes the tricky business of disambiguation.  At this
        # point, we know s is a proper prefix of word, eg. s='--foo' and
        # word=='--foobar'.  If we have already seen another word where
        # this was the case, eg. '--foobaz', fail: s is ambiguous.
        # Otherwise record this match and keep looping; we will return
        # if we see an exact match, or when we fall out of the loop and
        # it turns out that the current word is the match.
        if match:
            raise BadOptionError("ambiguous option: %s (%s, %s, ...?)"
                                 % (s, match, word))
        match = word

    if match:
        return match
    else:
        raise BadOptionError("no such option: %s" % s)