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