#!/usr/bin/env python # CommandLineApp.py """Base class for building command line applications. """ import getopt import inspect import os try: from cStringIO import StringIO except: from StringIO import StringIO import sys import textwrap class CommandLineApp(object): """Base class for building command line applications. Define a docstring for the class to explain what the program does. Include descriptions of the command arguments in the docstring for main(). When the EXAMPLES_DESCRIPTION class attribute is not empty, it will be printed last in the help message when the user asks for help. """ EXAMPLES_DESCRIPTION = '' # If true, always ends run() with sys.exit() force_exit = True # The name of this application _app_name = os.path.basename(sys.argv[0]) _app_version = None def __init__(self, commandLineOptions=sys.argv[1:]): "Initialize CommandLineApp." self.command_line_options = commandLineOptions self.supported_options = self.scanForOptions() return def main(self, *args): """Main body of your application. This is the main portion of the app, and is run after all of the arguments are processed. Override this method to implment the primary processing section of your application. """ pass def handleInterrupt(self): """Called when the program is interrupted via Control-C or SIGINT. Returns exit code. """ sys.stderr.write('Canceled by user.\n') return 1 def handleMainException(self, err): """Invoked when there is an error in the main() method. """ if self.debugging: import traceback traceback.print_exc() else: self.errorMessage(str(err)) return 1 ## HELP def showHelp(self, errorMessage=None): "Display help message when error occurs." print if self._app_version: print '%s version %s' % (self._app_name, self._app_version) else: print self._app_name print # If they made a syntax mistake, just # show them how to use the program. Otherwise, # show the full help message. if errorMessage: print '' print 'ERROR: ', errorMessage print '' print '' print '%s\n' % self._app_name print '' txt = self.getSimpleSyntaxHelpString() print txt print 'For more details, use --help.' print return def showVerboseHelp(self): "Display the full help text for the command." txt = self.getVerboseSyntaxHelpString() print txt return ## STATUS MESSAGES def statusMessage(self, msg='', verbose_level=1, error=False, newline=True): """Print a status message to output. Arguments msg='' -- The status message string to be printed. verbose_level=1 -- The verbose level to use. The message will only be printed if the current verbose level is >= this number. error=False -- If true, the message is considered an error and printed as such. newline=True -- If true, print a newline after the message. """ if self.verbose_level >= verbose_level: if error: output = sys.stderr else: output = sys.stdout output.write(str(msg)) if newline: output.write('\n') # some log mechanisms don't have a flush method if hasattr(output, 'flush'): output.flush() return def errorMessage(self, msg=''): 'Print a message as an error.' self.statusMessage('ERROR: %s\n' % msg, verbose_level=0, error=True) return ## DEFAULT OPTIONS debugging = False def optionHandler_debug(self): "Set debug mode to see tracebacks." self.debugging = True return _run_main = True def optionHandler_h(self): "Displays abbreviated help message." self.showHelp() self._run_main = False return def optionHandler_help(self): "Displays verbose help message." self.showVerboseHelp() self._run_main = False return def optionHandler_quiet(self): 'Turn on quiet mode.' self.verbose_level = 0 return verbose_level = 1 def optionHandler_v(self): """Increment the verbose level. Higher levels are more verbose. The default is 1. """ self.verbose_level = self.verbose_level + 1 self.statusMessage('New verbose level is %d' % self.verbose_level, 3) return def optionHandler_verbose(self, level=1): """Set the verbose level. """ self.verbose_level = int(level) self.statusMessage('New verbose level is %d' % self.verbose_level, 3) return ## INTERNALS (Subclasses should not need to override these methods) def run(self): """Entry point. Process options and execute callback functions as needed. This method should not need to be overridden, if the main() method is defined. """ # Process the options supported and given options = {} for info in self.supported_options: options[ info.switch ] = info parsed_options, remaining_args = self.callGetopt(self.command_line_options, self.supported_options) exit_code = 0 try: for switch, option_value in parsed_options: opt_def = options[switch] opt_def.invoke(self, option_value) # Perform the primary action for this application, # unless one of the options has disabled it. if self._run_main: main_args = tuple(remaining_args) # We could just call main() and catch a TypeError, # but that would not let us differentiate between # application errors and a case where the user # has not passed us enough arguments. So, we check # the argument count ourself. num_args_ok = False argspec = inspect.getargspec(self.main) expected_arg_count = len(argspec[0]) - 1 if argspec[1] is not None: num_args_ok = True if len(argspec[0]) > 1: num_args_ok = (len(main_args) >= expected_arg_count) elif len(main_args) == expected_arg_count: num_args_ok = True if num_args_ok: exit_code = self.main(*main_args) else: self.showHelp('Incorrect arguments.') exit_code = 1 except KeyboardInterrupt: exit_code = self.handleInterrupt() except SystemExit, msg: exit_code = msg.args[0] except Exception, err: exit_code = self.handleMainException(err) if self.debugging: raise if self.force_exit: sys.exit(exit_code) return exit_code def scanForOptions(self): "Scan through the inheritence hierarchy to find option handlers." options = [] methods = inspect.getmembers(self.__class__, inspect.ismethod) for method_name, method in methods: if method_name.startswith(OptionDef.OPTION_HANDLER_PREFIX): options.append(OptionDef(method_name, method)) return options def callGetopt(self, commandLineOptions, supportedOptions): "Parse the command line options." short_options = [] long_options = [] for o in supportedOptions: if len(o.option_name) == 1: short_options.append(o.option_name) if o.arg_name: short_options.append(':') elif o.arg_name: long_options.append('%s=' % o.switch_base) else: long_options.append(o.switch_base) short_option_string = ''.join(short_options) try: parsed_options, remaining_args = getopt.getopt( commandLineOptions, short_option_string, long_options) except getopt.error, message: self.showHelp(message) if self.force_exit: sys.exit(1) raise return (parsed_options, remaining_args) def _groupOptionAliases(self): """Return a sequence of tuples containing (option_names, option_defs) """ # Figure out which options are aliases option_aliases = {} for option in self.supported_options: method = getattr(self, option.method_name) existing_aliases = option_aliases.setdefault(method, []) existing_aliases.append(option) # Sort the groups in order grouped_options = [] for options in option_aliases.values(): names = [ o.option_name for o in options ] grouped_options.append( (names, options) ) grouped_options.sort() return grouped_options def _getOptionIdentifierText(self, options): """Return the option identifier text. For example: -h -v, --verbose -f bar, --foo bar """ option_texts = [] for option in options: option_texts.append(option.getSwitchText()) return ', '.join(option_texts) def getArgumentsSyntaxString(self): """Look at the arguments to main to see what the program accepts, and build a syntax string explaining how to pass those arguments. """ syntax_parts = [] argspec = inspect.getargspec(self.main) args = argspec[0] if len(args) > 1: for arg in args[1:]: syntax_parts.append(arg) if argspec[1]: syntax_parts.append(argspec[1]) syntax_parts.append('[' + argspec[1] + '...]') syntax = ' '.join(syntax_parts) return syntax def getSimpleSyntaxHelpString(self): """Return syntax statement. Return a simplified form of help including only the syntax of the command. """ buffer = StringIO() # Show the name of the command and basic syntax. buffer.write('%s [] %s\n\n' % \ (self._app_name, self.getArgumentsSyntaxString()) ) grouped_options = self._groupOptionAliases() # Assemble the text for the options for names, options in grouped_options: buffer.write(' %s\n' % self._getOptionIdentifierText(options)) return buffer.getvalue() def _formatHelpText(self, text, prefix): if not text: return '' buffer = StringIO() text = textwrap.dedent(text) for para in text.split('\n\n'): formatted_para = textwrap.fill(para, initial_indent=prefix, subsequent_indent=prefix, ) buffer.write(formatted_para) buffer.write('\n\n') return buffer.getvalue() def getVerboseSyntaxHelpString(self): """Return the full description of the options and arguments. Show a full description of the options and arguments to the command in something like UNIX man page format. This includes - a description of each option and argument, taken from the __doc__ string for the optionHandler method for the option - a description of what additional arguments will be processed, taken from the arguments to main() """ buffer = StringIO() class_help_text = self._formatHelpText(inspect.getdoc(self.__class__), '') buffer.write(class_help_text) buffer.write('\nSYNTAX:\n\n ') buffer.write(self.getSimpleSyntaxHelpString()) main_help_text = self._formatHelpText(inspect.getdoc(self.main), ' ') if main_help_text: buffer.write('\n\nARGUMENTS:\n\n') buffer.write(main_help_text) buffer.write('\nOPTIONS:\n\n') grouped_options = self._groupOptionAliases() # Describe all options, grouping aliases together for names, options in grouped_options: buffer.write(' %s\n' % self._getOptionIdentifierText(options)) help = self._formatHelpText(options[0].help, ' ') buffer.write(help) if self.EXAMPLES_DESCRIPTION: buffer.write('EXAMPLES:\n\n') buffer.write(self.EXAMPLES_DESCRIPTION) return buffer.getvalue() class OptionDef(object): """Definition for a command line option. Attributes: method_name - The name of the option handler method. option_name - The name of the option. switch - Switch to be used on the command line. arg_name - The name of the argument to the option handler. is_variable - Is the argument expected to be a sequence? default - The default value of the option handler argument. help - Help text for the option. is_long - Is the option a long value (--) or short (-)? """ # Option handler method names start with this value OPTION_HANDLER_PREFIX = 'optionHandler_' # For *args arguments to option handlers, how to split the argument values SPLIT_PARAM_CHAR = ',' def __init__(self, methodName, method): self.method_name = methodName self.option_name = methodName[len(self.OPTION_HANDLER_PREFIX):] self.is_long = len(self.option_name) > 1 self.switch_base = self.option_name.replace('_', '-') if len(self.switch_base) == 1: self.switch = '-' + self.switch_base else: self.switch = '--' + self.switch_base argspec = inspect.getargspec(method) self.is_variable = False args = argspec[0] if len(args) > 1: self.arg_name = args[-1] elif argspec[1]: self.arg_name = argspec[1] self.is_variable = True else: self.arg_name = None if argspec[3]: self.default = argspec[3][0] else: self.default = None self.help = inspect.getdoc(method) return def getSwitchText(self): """Return the description of the option switch. For example: --switch=arg or -s arg or --switch=arg[,arg] """ parts = [ self.switch ] if self.arg_name: if self.is_long: parts.append('=') else: parts.append(' ') parts.append(self.arg_name) if self.is_variable: parts.append('[%s%s...]' % (self.SPLIT_PARAM_CHAR, self.arg_name)) return ''.join(parts) def invoke(self, app, arg): """Invoke the option handler. """ method = getattr(app, self.method_name) if self.arg_name: if self.is_variable: opt_args = arg.split(self.SPLIT_PARAM_CHAR) method(*opt_args) else: method(arg) else: method() return if __name__ == '__main__': CommandLineApp().run()