| """ CommandLine - Get and parse command line options |
| |
| NOTE: This still is very much work in progress !!! |
| |
| Different version are likely to be incompatible. |
| |
| TODO: |
| |
| * Incorporate the changes made by (see Inbox) |
| * Add number range option using srange() |
| |
| """ |
| |
| from __future__ import print_function |
| |
| __copyright__ = """\ |
| Copyright (c), 1997-2006, Marc-Andre Lemburg (mal@lemburg.com) |
| Copyright (c), 2000-2006, eGenix.com Software GmbH (info@egenix.com) |
| See the documentation for further information on copyrights, |
| or contact the author. All Rights Reserved. |
| """ |
| |
| __version__ = '1.2' |
| |
| import sys, getopt, glob, os, re, traceback |
| |
| ### Helpers |
| |
| def _getopt_flags(options): |
| |
| """ Convert the option list to a getopt flag string and long opt |
| list |
| |
| """ |
| s = [] |
| l = [] |
| for o in options: |
| if o.prefix == '-': |
| # short option |
| s.append(o.name) |
| if o.takes_argument: |
| s.append(':') |
| else: |
| # long option |
| if o.takes_argument: |
| l.append(o.name+'=') |
| else: |
| l.append(o.name) |
| return ''.join(s), l |
| |
| def invisible_input(prompt='>>> '): |
| |
| """ Get raw input from a terminal without echoing the characters to |
| the terminal, e.g. for password queries. |
| |
| """ |
| import getpass |
| entry = getpass.getpass(prompt) |
| if entry is None: |
| raise KeyboardInterrupt |
| return entry |
| |
| def fileopen(name, mode='wb', encoding=None): |
| |
| """ Open a file using mode. |
| |
| Default mode is 'wb' meaning to open the file for writing in |
| binary mode. If encoding is given, I/O to and from the file is |
| transparently encoded using the given encoding. |
| |
| Files opened for writing are chmod()ed to 0600. |
| |
| """ |
| if name == 'stdout': |
| return sys.stdout |
| elif name == 'stderr': |
| return sys.stderr |
| elif name == 'stdin': |
| return sys.stdin |
| else: |
| if encoding is not None: |
| import codecs |
| f = codecs.open(name, mode, encoding) |
| else: |
| f = open(name, mode) |
| if 'w' in mode: |
| os.chmod(name, 0o600) |
| return f |
| |
| def option_dict(options): |
| |
| """ Return a dictionary mapping option names to Option instances. |
| """ |
| d = {} |
| for option in options: |
| d[option.name] = option |
| return d |
| |
| # Alias |
| getpasswd = invisible_input |
| |
| _integerRE = re.compile('\s*(-?\d+)\s*$') |
| _integerRangeRE = re.compile('\s*(-?\d+)\s*-\s*(-?\d+)\s*$') |
| |
| def srange(s, |
| |
| integer=_integerRE, |
| integerRange=_integerRangeRE): |
| |
| """ Converts a textual representation of integer numbers and ranges |
| to a Python list. |
| |
| Supported formats: 2,3,4,2-10,-1 - -3, 5 - -2 |
| |
| Values are appended to the created list in the order specified |
| in the string. |
| |
| """ |
| l = [] |
| append = l.append |
| for entry in s.split(','): |
| m = integer.match(entry) |
| if m: |
| append(int(m.groups()[0])) |
| continue |
| m = integerRange.match(entry) |
| if m: |
| start,end = map(int,m.groups()) |
| l[len(l):] = range(start,end+1) |
| return l |
| |
| def abspath(path, |
| |
| expandvars=os.path.expandvars,expanduser=os.path.expanduser, |
| join=os.path.join,getcwd=os.getcwd): |
| |
| """ Return the corresponding absolute path for path. |
| |
| path is expanded in the usual shell ways before |
| joining it with the current working directory. |
| |
| """ |
| try: |
| path = expandvars(path) |
| except AttributeError: |
| pass |
| try: |
| path = expanduser(path) |
| except AttributeError: |
| pass |
| return join(getcwd(), path) |
| |
| ### Option classes |
| |
| class Option: |
| |
| """ Option base class. Takes no argument. |
| |
| """ |
| default = None |
| helptext = '' |
| prefix = '-' |
| takes_argument = 0 |
| has_default = 0 |
| tab = 15 |
| |
| def __init__(self,name,help=None): |
| |
| if not name[:1] == '-': |
| raise TypeError('option names must start with "-"') |
| if name[1:2] == '-': |
| self.prefix = '--' |
| self.name = name[2:] |
| else: |
| self.name = name[1:] |
| if help: |
| self.help = help |
| |
| def __str__(self): |
| |
| o = self |
| name = o.prefix + o.name |
| if o.takes_argument: |
| name = name + ' arg' |
| if len(name) > self.tab: |
| name = name + '\n' + ' ' * (self.tab + 1 + len(o.prefix)) |
| else: |
| name = '%-*s ' % (self.tab, name) |
| description = o.help |
| if o.has_default: |
| description = description + ' (%s)' % o.default |
| return '%s %s' % (name, description) |
| |
| class ArgumentOption(Option): |
| |
| """ Option that takes an argument. |
| |
| An optional default argument can be given. |
| |
| """ |
| def __init__(self,name,help=None,default=None): |
| |
| # Basemethod |
| Option.__init__(self,name,help) |
| |
| if default is not None: |
| self.default = default |
| self.has_default = 1 |
| self.takes_argument = 1 |
| |
| class SwitchOption(Option): |
| |
| """ Options that can be on or off. Has an optional default value. |
| |
| """ |
| def __init__(self,name,help=None,default=None): |
| |
| # Basemethod |
| Option.__init__(self,name,help) |
| |
| if default is not None: |
| self.default = default |
| self.has_default = 1 |
| |
| ### Application baseclass |
| |
| class Application: |
| |
| """ Command line application interface with builtin argument |
| parsing. |
| |
| """ |
| # Options the program accepts (Option instances) |
| options = [] |
| |
| # Standard settings; these are appended to options in __init__ |
| preset_options = [SwitchOption('-v', |
| 'generate verbose output'), |
| SwitchOption('-h', |
| 'show this help text'), |
| SwitchOption('--help', |
| 'show this help text'), |
| SwitchOption('--debug', |
| 'enable debugging'), |
| SwitchOption('--copyright', |
| 'show copyright'), |
| SwitchOption('--examples', |
| 'show examples of usage')] |
| |
| # The help layout looks like this: |
| # [header] - defaults to '' |
| # |
| # [synopsis] - formatted as '<self.name> %s' % self.synopsis |
| # |
| # options: |
| # [options] - formatted from self.options |
| # |
| # [version] - formatted as 'Version:\n %s' % self.version, if given |
| # |
| # [about] - defaults to '' |
| # |
| # Note: all fields that do not behave as template are formatted |
| # using the instances dictionary as substitution namespace, |
| # e.g. %(name)s will be replaced by the applications name. |
| # |
| |
| # Header (default to program name) |
| header = '' |
| |
| # Name (defaults to program name) |
| name = '' |
| |
| # Synopsis (%(name)s is replaced by the program name) |
| synopsis = '%(name)s [option] files...' |
| |
| # Version (optional) |
| version = '' |
| |
| # General information printed after the possible options (optional) |
| about = '' |
| |
| # Examples of usage to show when the --examples option is given (optional) |
| examples = '' |
| |
| # Copyright to show |
| copyright = __copyright__ |
| |
| # Apply file globbing ? |
| globbing = 1 |
| |
| # Generate debug output ? |
| debug = 0 |
| |
| # Generate verbose output ? |
| verbose = 0 |
| |
| # Internal errors to catch |
| InternalError = BaseException |
| |
| # Instance variables: |
| values = None # Dictionary of passed options (or default values) |
| # indexed by the options name, e.g. '-h' |
| files = None # List of passed filenames |
| optionlist = None # List of passed options |
| |
| def __init__(self,argv=None): |
| |
| # Setup application specs |
| if argv is None: |
| argv = sys.argv |
| self.filename = os.path.split(argv[0])[1] |
| if not self.name: |
| self.name = os.path.split(self.filename)[1] |
| else: |
| self.name = self.name |
| if not self.header: |
| self.header = self.name |
| else: |
| self.header = self.header |
| |
| # Init .arguments list |
| self.arguments = argv[1:] |
| |
| # Setup Option mapping |
| self.option_map = option_dict(self.options) |
| |
| # Append preset options |
| for option in self.preset_options: |
| if not option.name in self.option_map: |
| self.add_option(option) |
| |
| # Init .files list |
| self.files = [] |
| |
| # Start Application |
| rc = 0 |
| try: |
| # Process startup |
| rc = self.startup() |
| if rc is not None: |
| raise SystemExit(rc) |
| |
| # Parse command line |
| rc = self.parse() |
| if rc is not None: |
| raise SystemExit(rc) |
| |
| # Start application |
| rc = self.main() |
| if rc is None: |
| rc = 0 |
| |
| except SystemExit as rcException: |
| rc = rcException |
| pass |
| |
| except KeyboardInterrupt: |
| print() |
| print('* User Break') |
| print() |
| rc = 1 |
| |
| except self.InternalError: |
| print() |
| print('* Internal Error (use --debug to display the traceback)') |
| if self.debug: |
| print() |
| traceback.print_exc(20, sys.stdout) |
| elif self.verbose: |
| print(' %s: %s' % sys.exc_info()[:2]) |
| print() |
| rc = 1 |
| |
| raise SystemExit(rc) |
| |
| def add_option(self, option): |
| |
| """ Add a new Option instance to the Application dynamically. |
| |
| Note that this has to be done *before* .parse() is being |
| executed. |
| |
| """ |
| self.options.append(option) |
| self.option_map[option.name] = option |
| |
| def startup(self): |
| |
| """ Set user defined instance variables. |
| |
| If this method returns anything other than None, the |
| process is terminated with the return value as exit code. |
| |
| """ |
| return None |
| |
| def exit(self, rc=0): |
| |
| """ Exit the program. |
| |
| rc is used as exit code and passed back to the calling |
| program. It defaults to 0 which usually means: OK. |
| |
| """ |
| raise SystemExit(rc) |
| |
| def parse(self): |
| |
| """ Parse the command line and fill in self.values and self.files. |
| |
| After having parsed the options, the remaining command line |
| arguments are interpreted as files and passed to .handle_files() |
| for processing. |
| |
| As final step the option handlers are called in the order |
| of the options given on the command line. |
| |
| """ |
| # Parse arguments |
| self.values = values = {} |
| for o in self.options: |
| if o.has_default: |
| values[o.prefix+o.name] = o.default |
| else: |
| values[o.prefix+o.name] = 0 |
| flags,lflags = _getopt_flags(self.options) |
| try: |
| optlist,files = getopt.getopt(self.arguments,flags,lflags) |
| if self.globbing: |
| l = [] |
| for f in files: |
| gf = glob.glob(f) |
| if not gf: |
| l.append(f) |
| else: |
| l[len(l):] = gf |
| files = l |
| self.optionlist = optlist |
| self.files = files + self.files |
| except getopt.error as why: |
| self.help(why) |
| sys.exit(1) |
| |
| # Call file handler |
| rc = self.handle_files(self.files) |
| if rc is not None: |
| sys.exit(rc) |
| |
| # Call option handlers |
| for optionname, value in optlist: |
| |
| # Try to convert value to integer |
| try: |
| value = int(value) |
| except ValueError: |
| pass |
| |
| # Find handler and call it (or count the number of option |
| # instances on the command line) |
| handlername = 'handle' + optionname.replace('-', '_') |
| try: |
| handler = getattr(self, handlername) |
| except AttributeError: |
| if value == '': |
| # count the number of occurances |
| if optionname in values: |
| values[optionname] = values[optionname] + 1 |
| else: |
| values[optionname] = 1 |
| else: |
| values[optionname] = value |
| else: |
| rc = handler(value) |
| if rc is not None: |
| raise SystemExit(rc) |
| |
| # Apply final file check (for backward compatibility) |
| rc = self.check_files(self.files) |
| if rc is not None: |
| sys.exit(rc) |
| |
| def check_files(self,filelist): |
| |
| """ Apply some user defined checks on the files given in filelist. |
| |
| This may modify filelist in place. A typical application |
| is checking that at least n files are given. |
| |
| If this method returns anything other than None, the |
| process is terminated with the return value as exit code. |
| |
| """ |
| return None |
| |
| def help(self,note=''): |
| |
| self.print_header() |
| if self.synopsis: |
| print('Synopsis:') |
| # To remain backward compatible: |
| try: |
| synopsis = self.synopsis % self.name |
| except (NameError, KeyError, TypeError): |
| synopsis = self.synopsis % self.__dict__ |
| print(' ' + synopsis) |
| print() |
| self.print_options() |
| if self.version: |
| print('Version:') |
| print(' %s' % self.version) |
| print() |
| if self.about: |
| about = self.about % self.__dict__ |
| print(about.strip()) |
| print() |
| if note: |
| print('-'*72) |
| print('Note:',note) |
| print() |
| |
| def notice(self,note): |
| |
| print('-'*72) |
| print('Note:',note) |
| print('-'*72) |
| print() |
| |
| def print_header(self): |
| |
| print('-'*72) |
| print(self.header % self.__dict__) |
| print('-'*72) |
| print() |
| |
| def print_options(self): |
| |
| options = self.options |
| print('Options and default settings:') |
| if not options: |
| print(' None') |
| return |
| int = [x for x in options if x.prefix == '--'] |
| short = [x for x in options if x.prefix == '-'] |
| items = short + int |
| for o in options: |
| print(' ',o) |
| print() |
| |
| # |
| # Example handlers: |
| # |
| # If a handler returns anything other than None, processing stops |
| # and the return value is passed to sys.exit() as argument. |
| # |
| |
| # File handler |
| def handle_files(self,files): |
| |
| """ This may process the files list in place. |
| """ |
| return None |
| |
| # Short option handler |
| def handle_h(self,arg): |
| |
| self.help() |
| return 0 |
| |
| def handle_v(self, value): |
| |
| """ Turn on verbose output. |
| """ |
| self.verbose = 1 |
| |
| # Handlers for long options have two underscores in their name |
| def handle__help(self,arg): |
| |
| self.help() |
| return 0 |
| |
| def handle__debug(self,arg): |
| |
| self.debug = 1 |
| # We don't want to catch internal errors: |
| class NoErrorToCatch(Exception): pass |
| self.InternalError = NoErrorToCatch |
| |
| def handle__copyright(self,arg): |
| |
| self.print_header() |
| copyright = self.copyright % self.__dict__ |
| print(copyright.strip()) |
| print() |
| return 0 |
| |
| def handle__examples(self,arg): |
| |
| self.print_header() |
| if self.examples: |
| print('Examples:') |
| print() |
| examples = self.examples % self.__dict__ |
| print(examples.strip()) |
| print() |
| else: |
| print('No examples available.') |
| print() |
| return 0 |
| |
| def main(self): |
| |
| """ Override this method as program entry point. |
| |
| The return value is passed to sys.exit() as argument. If |
| it is None, 0 is assumed (meaning OK). Unhandled |
| exceptions are reported with exit status code 1 (see |
| __init__ for further details). |
| |
| """ |
| return None |
| |
| # Alias |
| CommandLine = Application |
| |
| def _test(): |
| |
| class MyApplication(Application): |
| header = 'Test Application' |
| version = __version__ |
| options = [Option('-v','verbose')] |
| |
| def handle_v(self,arg): |
| print('VERBOSE, Yeah !') |
| |
| cmd = MyApplication() |
| if not cmd.values['-h']: |
| cmd.help() |
| print('files:',cmd.files) |
| print('Bye...') |
| |
| if __name__ == '__main__': |
| _test() |