Source code for pycommand.pycommand

'''Library / toolkit for creating command line programs with minimal effort.'''

# Copyright (c) 2013-2016, 2018  Benjamin Althues <benjamin@babab.nl>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

from __future__ import absolute_import

__docformat__ = 'restructuredtext'
__author__ = "Benjamin Althues"
__copyright__ = "Copyright (C) 2013-2016, 2018  Benjamin Althues"
__version_info__ = (0, 4, 0, 'final', 0)
__version__ = '0.4.0'


from collections import OrderedDict
import getopt
import sys


class CommandExit(Exception):
    def __init__(self, val):
        self.err = val

    def __str__(self):
        return repr(self.err)


class OptionError(AttributeError):
    '''Options/Flags AttributeError exception'''


class dictobject(dict):
    '''A dictionary with getters by attribute, used for flags '''
    def __getattr__(self, name):
        if name in self:
            return self[name]
        else:
            raise OptionError("Option '{}' is not defined".format(name))


[docs]class CommandBase(object): '''Base class for (sub)commands''' usagestr = 'usage: command [options]' '''String. Usage synopsis''' description = '' '''String. Small description of subcommand''' optionList = {} '''Dictionary of options (as a tuple of 2-tuples). This will be transformed to an OrderedDict when initializing the object. Example:: optionList = ( ('help', ('h', False, 'show this help information')), ('dry-run', ('n', False, 'only print output without actually running')), # To specify that an option requires an argument # just add a string that describes it ('file', ('f', '<filename>', 'use specified file')), # Use an empty string to ommit short option ('debug', ('', False, 'show debug information')), ) ''' usageTextExtra = '' '''String. Optional extra usage information''' commands = {} '''Dictionary of commands and the callables they invoke.''' def __init__(self, argv=sys.argv[1:]): '''Initialize (sub)command object :Parameters: - `argv`: List of arguments. E.g. `sys.argv[1:]` ''' # Instance vars self.error = None '''Thrown by GetoptError when parsing illegal arguments.''' self.flags = {} '''Dict of parsed options and corresponding arguments, if any.''' self.args = [] '''List of parsed postional arguments''' self.parentFlags = {} '''Dict of registered `flags` of parent Command object.''' self.usage = '' '''String with usage information The string is compiled using the values found for `usagestr`, `description`, `optionList` and `usageTextExtra`. ''' self.optionList = OrderedDict(self.optionList) # Local vars longopts = [] padding = 0 shortopts = '' # Calculate padding needed for option arguments in usage info for flag, val in self.optionList.items(): optlen = len(flag) + 2 optlen += 4 if val[0] else 0 optlen += len(val[1]) + 1 if val[0] and val[1] else 0 optlen += len(val[1]) + 1 if val[1] else 0 padding = optlen if optlen > padding else padding # Create usage information and build dict of possible flags opthelp = '' for flag, val in self.optionList.items(): spec = flag + '=' if val[1] else flag longopts.append(spec) self.flags.update({flag: None}) if val[1]: flagstring_long = ('{flag}={argument}' .format(flag=flag, argument=val[1])) if val[0]: flagstring_short = ('{flag} {argument}' .format(flag=val[0], argument=val[1])) else: flagstring_long = flag flagstring_short = val[0] if val[0]: shortopts += val[0] + ':' if val[1] else val[0] optline = ('-{short}, --{flag}' .format(short=flagstring_short, flag=flagstring_long)) else: optline = '--{flag}'.format(flag=flagstring_long) opthelp += ('{options:{padding}} {desc}\n' .format(options=optline, padding=padding, desc=val[2])) self.usage = self.usagestr if self.description: self.usage += '\n\n{desc}'.format(desc=self.description) if self.optionList: self.usage += '\n\nOptions:\n{opts}'.format(opts=opthelp) if self.usageTextExtra: self.usage += '\n{help}'.format(help=self.usageTextExtra) # Parse arguments and options try: opts, self.args = getopt.getopt(argv, shortopts, longopts) except getopt.GetoptError as err: self.error = err return # Stop when an invalid option is parsed for opt in opts: # Compare each option with optionList and set values for flags for flag, val in self.optionList.items(): if opt[0][1] != '-': # Short tags if opt[0][1] == val[0]: if val[1]: self.flags[flag] = opt[1] else: self.flags[flag] = True else: # Long tags if opt[0][2:] == flag: if val[1]: self.flags[flag] = opt[1] else: self.flags[flag] = True # Convert to dictobject to allow getting flags by attribute name self.flags = dictobject(self.flags) def run(self): if not self.args: print(self.usage) raise CommandExit(2) elif self.args[0] in self.commands: return self.commands[self.args[0]](argv=self.args[1:]) else: print('error: command {cmd} does not exist' .format(cmd=self.args[0])) raise CommandExit(1)
[docs] def registerParentFlag(self, optionName, value): '''Register a flag of a parent command :Parameters: - `optionName`: String. Name of option - `value`: Mixed. Value of parsed flag` ''' self.parentFlags.update({optionName: value}) return self
[docs]def run_and_exit(command_class): '''A shortcut for reading from sys.argv and exiting the interpreter''' cmd = command_class(sys.argv[1:]) if cmd.error: print('error: {0}'.format(cmd.error)) sys.exit(1) else: sys.exit(cmd.run())