1""" CommandLine - Get and parse command line options
2
3    NOTE: This still is very much work in progress !!!
4
5    Different version are likely to be incompatible.
6
7    TODO:
8
9    * Incorporate the changes made by (see Inbox)
10    * Add number range option using srange()
11
12"""
13
14__copyright__ = """\
15Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com)
16Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com)
17See the documentation for further information on copyrights,
18or contact the author. All Rights Reserved.
19"""
20
21__version__ = '1.2'
22
23import sys, getopt, string, glob, os, re, exceptions, traceback
24
25### Helpers
26
27def _getopt_flags(options):
28
29    """ Convert the option list to a getopt flag string and long opt
30        list
31
32    """
33    s = []
34    l = []
35    for o in options:
36        if o.prefix == '-':
37            # short option
38            s.append(o.name)
39            if o.takes_argument:
40                s.append(':')
41        else:
42            # long option
43            if o.takes_argument:
44                l.append(o.name+'=')
45            else:
46                l.append(o.name)
47    return string.join(s,''),l
48
49def invisible_input(prompt='>>> '):
50
51    """ Get raw input from a terminal without echoing the characters to
52        the terminal, e.g. for password queries.
53
54    """
55    import getpass
56    entry = getpass.getpass(prompt)
57    if entry is None:
58        raise KeyboardInterrupt
59    return entry
60
61def fileopen(name, mode='wb', encoding=None):
62
63    """ Open a file using mode.
64
65        Default mode is 'wb' meaning to open the file for writing in
66        binary mode. If encoding is given, I/O to and from the file is
67        transparently encoded using the given encoding.
68
69        Files opened for writing are chmod()ed to 0600.
70
71    """
72    if name == 'stdout':
73        return sys.stdout
74    elif name == 'stderr':
75        return sys.stderr
76    elif name == 'stdin':
77        return sys.stdin
78    else:
79        if encoding is not None:
80            import codecs
81            f = codecs.open(name, mode, encoding)
82        else:
83            f = open(name, mode)
84        if 'w' in mode:
85            os.chmod(name, 0600)
86        return f
87
88def option_dict(options):
89
90    """ Return a dictionary mapping option names to Option instances.
91    """
92    d = {}
93    for option in options:
94        d[option.name] = option
95    return d
96
97# Alias
98getpasswd = invisible_input
99
100_integerRE = re.compile('\s*(-?\d+)\s*$')
101_integerRangeRE = re.compile('\s*(-?\d+)\s*-\s*(-?\d+)\s*$')
102
103def srange(s,
104
105           split=string.split,integer=_integerRE,
106           integerRange=_integerRangeRE):
107
108    """ Converts a textual representation of integer numbers and ranges
109        to a Python list.
110
111        Supported formats: 2,3,4,2-10,-1 - -3, 5 - -2
112
113        Values are appended to the created list in the order specified
114        in the string.
115
116    """
117    l = []
118    append = l.append
119    for entry in split(s,','):
120        m = integer.match(entry)
121        if m:
122            append(int(m.groups()[0]))
123            continue
124        m = integerRange.match(entry)
125        if m:
126            start,end = map(int,m.groups())
127            l[len(l):] = range(start,end+1)
128    return l
129
130def abspath(path,
131
132            expandvars=os.path.expandvars,expanduser=os.path.expanduser,
133            join=os.path.join,getcwd=os.getcwd):
134
135    """ Return the corresponding absolute path for path.
136
137        path is expanded in the usual shell ways before
138        joining it with the current working directory.
139
140    """
141    try:
142        path = expandvars(path)
143    except AttributeError:
144        pass
145    try:
146        path = expanduser(path)
147    except AttributeError:
148        pass
149    return join(getcwd(), path)
150
151### Option classes
152
153class Option:
154
155    """ Option base class. Takes no argument.
156
157    """
158    default = None
159    helptext = ''
160    prefix = '-'
161    takes_argument = 0
162    has_default = 0
163    tab = 15
164
165    def __init__(self,name,help=None):
166
167        if not name[:1] == '-':
168            raise TypeError,'option names must start with "-"'
169        if name[1:2] == '-':
170            self.prefix = '--'
171            self.name = name[2:]
172        else:
173            self.name = name[1:]
174        if help:
175            self.help = help
176
177    def __str__(self):
178
179        o = self
180        name = o.prefix + o.name
181        if o.takes_argument:
182            name = name + ' arg'
183        if len(name) > self.tab:
184            name = name + '\n' + ' ' * (self.tab + 1 + len(o.prefix))
185        else:
186            name = '%-*s ' % (self.tab, name)
187        description = o.help
188        if o.has_default:
189            description = description + ' (%s)' % o.default
190        return '%s %s' % (name, description)
191
192class ArgumentOption(Option):
193
194    """ Option that takes an argument.
195
196        An optional default argument can be given.
197
198    """
199    def __init__(self,name,help=None,default=None):
200
201        # Basemethod
202        Option.__init__(self,name,help)
203
204        if default is not None:
205            self.default = default
206            self.has_default = 1
207        self.takes_argument = 1
208
209class SwitchOption(Option):
210
211    """ Options that can be on or off. Has an optional default value.
212
213    """
214    def __init__(self,name,help=None,default=None):
215
216        # Basemethod
217        Option.__init__(self,name,help)
218
219        if default is not None:
220            self.default = default
221            self.has_default = 1
222
223### Application baseclass
224
225class Application:
226
227    """ Command line application interface with builtin argument
228        parsing.
229
230    """
231    # Options the program accepts (Option instances)
232    options = []
233
234    # Standard settings; these are appended to options in __init__
235    preset_options = [SwitchOption('-v',
236                                   'generate verbose output'),
237                      SwitchOption('-h',
238                                   'show this help text'),
239                      SwitchOption('--help',
240                                   'show this help text'),
241                      SwitchOption('--debug',
242                                   'enable debugging'),
243                      SwitchOption('--copyright',
244                                   'show copyright'),
245                      SwitchOption('--examples',
246                                   'show examples of usage')]
247
248    # The help layout looks like this:
249    # [header]   - defaults to ''
250    #
251    # [synopsis] - formatted as '<self.name> %s' % self.synopsis
252    #
253    # options:
254    # [options]  - formatted from self.options
255    #
256    # [version]  - formatted as 'Version:\n %s' % self.version, if given
257    #
258    # [about]    - defaults to ''
259    #
260    # Note: all fields that do not behave as template are formatted
261    #       using the instances dictionary as substitution namespace,
262    #       e.g. %(name)s will be replaced by the applications name.
263    #
264
265    # Header (default to program name)
266    header = ''
267
268    # Name (defaults to program name)
269    name = ''
270
271    # Synopsis (%(name)s is replaced by the program name)
272    synopsis = '%(name)s [option] files...'
273
274    # Version (optional)
275    version = ''
276
277    # General information printed after the possible options (optional)
278    about = ''
279
280    # Examples of usage to show when the --examples option is given (optional)
281    examples = ''
282
283    # Copyright to show
284    copyright = __copyright__
285
286    # Apply file globbing ?
287    globbing = 1
288
289    # Generate debug output ?
290    debug = 0
291
292    # Generate verbose output ?
293    verbose = 0
294
295    # Internal errors to catch
296    InternalError = exceptions.Exception
297
298    # Instance variables:
299    values = None       # Dictionary of passed options (or default values)
300                        # indexed by the options name, e.g. '-h'
301    files = None        # List of passed filenames
302    optionlist = None   # List of passed options
303
304    def __init__(self,argv=None):
305
306        # Setup application specs
307        if argv is None:
308            argv = sys.argv
309        self.filename = os.path.split(argv[0])[1]
310        if not self.name:
311            self.name = os.path.split(self.filename)[1]
312        else:
313            self.name = self.name
314        if not self.header:
315            self.header = self.name
316        else:
317            self.header = self.header
318
319        # Init .arguments list
320        self.arguments = argv[1:]
321
322        # Setup Option mapping
323        self.option_map = option_dict(self.options)
324
325        # Append preset options
326        for option in self.preset_options:
327            if not self.option_map.has_key(option.name):
328                self.add_option(option)
329
330        # Init .files list
331        self.files = []
332
333        # Start Application
334        try:
335            # Process startup
336            rc = self.startup()
337            if rc is not None:
338                raise SystemExit,rc
339
340            # Parse command line
341            rc = self.parse()
342            if rc is not None:
343                raise SystemExit,rc
344
345            # Start application
346            rc = self.main()
347            if rc is None:
348                rc = 0
349
350        except SystemExit,rc:
351            pass
352
353        except KeyboardInterrupt:
354            print
355            print '* User Break'
356            print
357            rc = 1
358
359        except self.InternalError:
360            print
361            print '* Internal Error (use --debug to display the traceback)'
362            if self.debug:
363                print
364                traceback.print_exc(20, sys.stdout)
365            elif self.verbose:
366                print '  %s: %s' % sys.exc_info()[:2]
367            print
368            rc = 1
369
370        raise SystemExit,rc
371
372    def add_option(self, option):
373
374        """ Add a new Option instance to the Application dynamically.
375
376            Note that this has to be done *before* .parse() is being
377            executed.
378
379        """
380        self.options.append(option)
381        self.option_map[option.name] = option
382
383    def startup(self):
384
385        """ Set user defined instance variables.
386
387            If this method returns anything other than None, the
388            process is terminated with the return value as exit code.
389
390        """
391        return None
392
393    def exit(self, rc=0):
394
395        """ Exit the program.
396
397            rc is used as exit code and passed back to the calling
398            program. It defaults to 0 which usually means: OK.
399
400        """
401        raise SystemExit, rc
402
403    def parse(self):
404
405        """ Parse the command line and fill in self.values and self.files.
406
407            After having parsed the options, the remaining command line
408            arguments are interpreted as files and passed to .handle_files()
409            for processing.
410
411            As final step the option handlers are called in the order
412            of the options given on the command line.
413
414        """
415        # Parse arguments
416        self.values = values = {}
417        for o in self.options:
418            if o.has_default:
419                values[o.prefix+o.name] = o.default
420            else:
421                values[o.prefix+o.name] = 0
422        flags,lflags = _getopt_flags(self.options)
423        try:
424            optlist,files = getopt.getopt(self.arguments,flags,lflags)
425            if self.globbing:
426                l = []
427                for f in files:
428                    gf = glob.glob(f)
429                    if not gf:
430                        l.append(f)
431                    else:
432                        l[len(l):] = gf
433                files = l
434            self.optionlist = optlist
435            self.files = files + self.files
436        except getopt.error,why:
437            self.help(why)
438            sys.exit(1)
439
440        # Call file handler
441        rc = self.handle_files(self.files)
442        if rc is not None:
443            sys.exit(rc)
444
445        # Call option handlers
446        for optionname, value in optlist:
447
448            # Try to convert value to integer
449            try:
450                value = string.atoi(value)
451            except ValueError:
452                pass
453
454            # Find handler and call it (or count the number of option
455            # instances on the command line)
456            handlername = 'handle' + string.replace(optionname, '-', '_')
457            try:
458                handler = getattr(self, handlername)
459            except AttributeError:
460                if value == '':
461                    # count the number of occurances
462                    if values.has_key(optionname):
463                        values[optionname] = values[optionname] + 1
464                    else:
465                        values[optionname] = 1
466                else:
467                    values[optionname] = value
468            else:
469                rc = handler(value)
470                if rc is not None:
471                    raise SystemExit, rc
472
473        # Apply final file check (for backward compatibility)
474        rc = self.check_files(self.files)
475        if rc is not None:
476            sys.exit(rc)
477
478    def check_files(self,filelist):
479
480        """ Apply some user defined checks on the files given in filelist.
481
482            This may modify filelist in place. A typical application
483            is checking that at least n files are given.
484
485            If this method returns anything other than None, the
486            process is terminated with the return value as exit code.
487
488        """
489        return None
490
491    def help(self,note=''):
492
493        self.print_header()
494        if self.synopsis:
495            print 'Synopsis:'
496            # To remain backward compatible:
497            try:
498                synopsis = self.synopsis % self.name
499            except (NameError, KeyError, TypeError):
500                synopsis = self.synopsis % self.__dict__
501            print ' ' + synopsis
502        print
503        self.print_options()
504        if self.version:
505            print 'Version:'
506            print ' %s' % self.version
507            print
508        if self.about:
509            print string.strip(self.about % self.__dict__)
510            print
511        if note:
512            print '-'*72
513            print 'Note:',note
514            print
515
516    def notice(self,note):
517
518        print '-'*72
519        print 'Note:',note
520        print '-'*72
521        print
522
523    def print_header(self):
524
525        print '-'*72
526        print self.header % self.__dict__
527        print '-'*72
528        print
529
530    def print_options(self):
531
532        options = self.options
533        print 'Options and default settings:'
534        if not options:
535            print '  None'
536            return
537        long = filter(lambda x: x.prefix == '--', options)
538        short = filter(lambda x: x.prefix == '-', options)
539        items = short + long
540        for o in options:
541            print ' ',o
542        print
543
544    #
545    # Example handlers:
546    #
547    # If a handler returns anything other than None, processing stops
548    # and the return value is passed to sys.exit() as argument.
549    #
550
551    # File handler
552    def handle_files(self,files):
553
554        """ This may process the files list in place.
555        """
556        return None
557
558    # Short option handler
559    def handle_h(self,arg):
560
561        self.help()
562        return 0
563
564    def handle_v(self, value):
565
566        """ Turn on verbose output.
567        """
568        self.verbose = 1
569
570    # Handlers for long options have two underscores in their name
571    def handle__help(self,arg):
572
573        self.help()
574        return 0
575
576    def handle__debug(self,arg):
577
578        self.debug = 1
579        # We don't want to catch internal errors:
580        self.InternalError = None
581
582    def handle__copyright(self,arg):
583
584        self.print_header()
585        print string.strip(self.copyright % self.__dict__)
586        print
587        return 0
588
589    def handle__examples(self,arg):
590
591        self.print_header()
592        if self.examples:
593            print 'Examples:'
594            print
595            print string.strip(self.examples % self.__dict__)
596            print
597        else:
598            print 'No examples available.'
599            print
600        return 0
601
602    def main(self):
603
604        """ Override this method as program entry point.
605
606            The return value is passed to sys.exit() as argument.  If
607            it is None, 0 is assumed (meaning OK). Unhandled
608            exceptions are reported with exit status code 1 (see
609            __init__ for further details).
610
611        """
612        return None
613
614# Alias
615CommandLine = Application
616
617def _test():
618
619    class MyApplication(Application):
620        header = 'Test Application'
621        version = __version__
622        options = [Option('-v','verbose')]
623
624        def handle_v(self,arg):
625            print 'VERBOSE, Yeah !'
626
627    cmd = MyApplication()
628    if not cmd.values['-h']:
629        cmd.help()
630    print 'files:',cmd.files
631    print 'Bye...'
632
633if __name__ == '__main__':
634    _test()
635