Home | History | Annotate | Download | only in pybench
      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 from __future__ import print_function
     15 
     16 __copyright__ = """\
     17 Copyright (c), 1997-2006, Marc-Andre Lemburg (mal (at] lemburg.com)
     18 Copyright (c), 2000-2006, eGenix.com Software GmbH (info (at] egenix.com)
     19 See the documentation for further information on copyrights,
     20 or contact the author. All Rights Reserved.
     21 """
     22 
     23 __version__ = '1.2'
     24 
     25 import sys, getopt, glob, os, re, traceback
     26 
     27 ### Helpers
     28 
     29 def _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 
     51 def 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 
     63 def 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 
     90 def 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
    100 getpasswd = invisible_input
    101 
    102 _integerRE = re.compile(r'\s*(-?\d+)\s*$')
    103 _integerRangeRE = re.compile(r'\s*(-?\d+)\s*-\s*(-?\d+)\s*$')
    104 
    105 def 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 
    132 def 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 
    155 class 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 
    194 class 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 
    211 class 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 
    227 class 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
    623 CommandLine = Application
    624 
    625 def _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 
    641 if __name__ == '__main__':
    642     _test()
    643