| Index: third_party/logilab/common/clcommands.py
|
| diff --git a/third_party/logilab/common/clcommands.py b/third_party/logilab/common/clcommands.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..411931bbfbc8e2b3bf3ec9faf82b4c719c633c0a
|
| --- /dev/null
|
| +++ b/third_party/logilab/common/clcommands.py
|
| @@ -0,0 +1,332 @@
|
| +# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
| +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
|
| +#
|
| +# This file is part of logilab-common.
|
| +#
|
| +# logilab-common is free software: you can redistribute it and/or modify it under
|
| +# the terms of the GNU Lesser General Public License as published by the Free
|
| +# Software Foundation, either version 2.1 of the License, or (at your option) any
|
| +# later version.
|
| +#
|
| +# logilab-common is distributed in the hope that it will be useful, but WITHOUT
|
| +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
| +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
| +# details.
|
| +#
|
| +# You should have received a copy of the GNU Lesser General Public License along
|
| +# with logilab-common. If not, see <http://www.gnu.org/licenses/>.
|
| +"""Helper functions to support command line tools providing more than
|
| +one command.
|
| +
|
| +e.g called as "tool command [options] args..." where <options> and <args> are
|
| +command'specific
|
| +"""
|
| +
|
| +__docformat__ = "restructuredtext en"
|
| +
|
| +import sys
|
| +import logging
|
| +from os.path import basename
|
| +
|
| +from logilab.common.configuration import Configuration
|
| +from logilab.common.logging_ext import init_log, get_threshold
|
| +from logilab.common.deprecation import deprecated
|
| +
|
| +
|
| +class BadCommandUsage(Exception):
|
| + """Raised when an unknown command is used or when a command is not
|
| + correctly used (bad options, too much / missing arguments...).
|
| +
|
| + Trigger display of command usage.
|
| + """
|
| +
|
| +class CommandError(Exception):
|
| + """Raised when a command can't be processed and we want to display it and
|
| + exit, without traceback nor usage displayed.
|
| + """
|
| +
|
| +
|
| +# command line access point ####################################################
|
| +
|
| +class CommandLine(dict):
|
| + """Usage:
|
| +
|
| + >>> LDI = cli.CommandLine('ldi', doc='Logilab debian installer',
|
| + version=version, rcfile=RCFILE)
|
| + >>> LDI.register(MyCommandClass)
|
| + >>> LDI.register(MyOtherCommandClass)
|
| + >>> LDI.run(sys.argv[1:])
|
| +
|
| + Arguments:
|
| +
|
| + * `pgm`, the program name, default to `basename(sys.argv[0])`
|
| +
|
| + * `doc`, a short description of the command line tool
|
| +
|
| + * `copyright`, additional doc string that will be appended to the generated
|
| + doc
|
| +
|
| + * `version`, version number of string of the tool. If specified, global
|
| + --version option will be available.
|
| +
|
| + * `rcfile`, path to a configuration file. If specified, global --C/--rc-file
|
| + option will be available? self.rcfile = rcfile
|
| +
|
| + * `logger`, logger to propagate to commands, default to
|
| + `logging.getLogger(self.pgm))`
|
| + """
|
| + def __init__(self, pgm=None, doc=None, copyright=None, version=None,
|
| + rcfile=None, logthreshold=logging.ERROR,
|
| + check_duplicated_command=True):
|
| + if pgm is None:
|
| + pgm = basename(sys.argv[0])
|
| + self.pgm = pgm
|
| + self.doc = doc
|
| + self.copyright = copyright
|
| + self.version = version
|
| + self.rcfile = rcfile
|
| + self.logger = None
|
| + self.logthreshold = logthreshold
|
| + self.check_duplicated_command = check_duplicated_command
|
| +
|
| + def register(self, cls, force=False):
|
| + """register the given :class:`Command` subclass"""
|
| + assert not self.check_duplicated_command or force or not cls.name in self, \
|
| + 'a command %s is already defined' % cls.name
|
| + self[cls.name] = cls
|
| + return cls
|
| +
|
| + def run(self, args):
|
| + """main command line access point:
|
| + * init logging
|
| + * handle global options (-h/--help, --version, -C/--rc-file)
|
| + * check command
|
| + * run command
|
| +
|
| + Terminate by :exc:`SystemExit`
|
| + """
|
| + init_log(debug=True, # so that we use StreamHandler
|
| + logthreshold=self.logthreshold,
|
| + logformat='%(levelname)s: %(message)s')
|
| + try:
|
| + arg = args.pop(0)
|
| + except IndexError:
|
| + self.usage_and_exit(1)
|
| + if arg in ('-h', '--help'):
|
| + self.usage_and_exit(0)
|
| + if self.version is not None and arg in ('--version'):
|
| + print self.version
|
| + sys.exit(0)
|
| + rcfile = self.rcfile
|
| + if rcfile is not None and arg in ('-C', '--rc-file'):
|
| + try:
|
| + rcfile = args.pop(0)
|
| + arg = args.pop(0)
|
| + except IndexError:
|
| + self.usage_and_exit(1)
|
| + try:
|
| + command = self.get_command(arg)
|
| + except KeyError:
|
| + print 'ERROR: no %s command' % arg
|
| + print
|
| + self.usage_and_exit(1)
|
| + try:
|
| + sys.exit(command.main_run(args, rcfile))
|
| + except KeyboardInterrupt, exc:
|
| + print 'Interrupted',
|
| + if str(exc):
|
| + print ': %s' % exc,
|
| + print
|
| + sys.exit(4)
|
| + except BadCommandUsage, err:
|
| + print 'ERROR:', err
|
| + print
|
| + print command.help()
|
| + sys.exit(1)
|
| +
|
| + def create_logger(self, handler, logthreshold=None):
|
| + logger = logging.Logger(self.pgm)
|
| + logger.handlers = [handler]
|
| + if logthreshold is None:
|
| + logthreshold = get_threshold(self.logthreshold)
|
| + logger.setLevel(logthreshold)
|
| + return logger
|
| +
|
| + def get_command(self, cmd, logger=None):
|
| + if logger is None:
|
| + logger = self.logger
|
| + if logger is None:
|
| + logger = self.logger = logging.getLogger(self.pgm)
|
| + logger.setLevel(get_threshold(self.logthreshold))
|
| + return self[cmd](logger)
|
| +
|
| + def usage(self):
|
| + """display usage for the main program (i.e. when no command supplied)
|
| + and exit
|
| + """
|
| + print 'usage:', self.pgm,
|
| + if self.rcfile:
|
| + print '[--rc-file=<configuration file>]',
|
| + print '<command> [options] <command argument>...'
|
| + if self.doc:
|
| + print '\n%s' % self.doc
|
| + print '''
|
| +Type "%(pgm)s <command> --help" for more information about a specific
|
| +command. Available commands are :\n''' % self.__dict__
|
| + max_len = max([len(cmd) for cmd in self])
|
| + padding = ' ' * max_len
|
| + for cmdname, cmd in sorted(self.items()):
|
| + if not cmd.hidden:
|
| + print ' ', (cmdname + padding)[:max_len], cmd.short_description()
|
| + if self.rcfile:
|
| + print '''
|
| +Use --rc-file=<configuration file> / -C <configuration file> before the command
|
| +to specify a configuration file. Default to %s.
|
| +''' % self.rcfile
|
| + print '''%(pgm)s -h/--help
|
| + display this usage information and exit''' % self.__dict__
|
| + if self.version:
|
| + print '''%(pgm)s -v/--version
|
| + display version configuration and exit''' % self.__dict__
|
| + if self.copyright:
|
| + print '\n', self.copyright
|
| +
|
| + def usage_and_exit(self, status):
|
| + self.usage()
|
| + sys.exit(status)
|
| +
|
| +
|
| +# base command classes #########################################################
|
| +
|
| +class Command(Configuration):
|
| + """Base class for command line commands.
|
| +
|
| + Class attributes:
|
| +
|
| + * `name`, the name of the command
|
| +
|
| + * `min_args`, minimum number of arguments, None if unspecified
|
| +
|
| + * `max_args`, maximum number of arguments, None if unspecified
|
| +
|
| + * `arguments`, string describing arguments, used in command usage
|
| +
|
| + * `hidden`, boolean flag telling if the command should be hidden, e.g. does
|
| + not appear in help's commands list
|
| +
|
| + * `options`, options list, as allowed by :mod:configuration
|
| + """
|
| +
|
| + arguments = ''
|
| + name = ''
|
| + # hidden from help ?
|
| + hidden = False
|
| + # max/min args, None meaning unspecified
|
| + min_args = None
|
| + max_args = None
|
| +
|
| + @classmethod
|
| + def description(cls):
|
| + return cls.__doc__.replace(' ', '')
|
| +
|
| + @classmethod
|
| + def short_description(cls):
|
| + return cls.description().split('.')[0]
|
| +
|
| + def __init__(self, logger):
|
| + usage = '%%prog %s %s\n\n%s' % (self.name, self.arguments,
|
| + self.description())
|
| + Configuration.__init__(self, usage=usage)
|
| + self.logger = logger
|
| +
|
| + def check_args(self, args):
|
| + """check command's arguments are provided"""
|
| + if self.min_args is not None and len(args) < self.min_args:
|
| + raise BadCommandUsage('missing argument')
|
| + if self.max_args is not None and len(args) > self.max_args:
|
| + raise BadCommandUsage('too many arguments')
|
| +
|
| + def main_run(self, args, rcfile=None):
|
| + """Run the command and return status 0 if everything went fine.
|
| +
|
| + If :exc:`CommandError` is raised by the underlying command, simply log
|
| + the error and return status 2.
|
| +
|
| + Any other exceptions, including :exc:`BadCommandUsage` will be
|
| + propagated.
|
| + """
|
| + if rcfile:
|
| + self.load_file_configuration(rcfile)
|
| + args = self.load_command_line_configuration(args)
|
| + try:
|
| + self.check_args(args)
|
| + self.run(args)
|
| + except CommandError, err:
|
| + self.logger.error(err)
|
| + return 2
|
| + return 0
|
| +
|
| + def run(self, args):
|
| + """run the command with its specific arguments"""
|
| + raise NotImplementedError()
|
| +
|
| +
|
| +class ListCommandsCommand(Command):
|
| + """list available commands, useful for bash completion."""
|
| + name = 'listcommands'
|
| + arguments = '[command]'
|
| + hidden = True
|
| +
|
| + def run(self, args):
|
| + """run the command with its specific arguments"""
|
| + if args:
|
| + command = args.pop()
|
| + cmd = _COMMANDS[command]
|
| + for optname, optdict in cmd.options:
|
| + print '--help'
|
| + print '--' + optname
|
| + else:
|
| + commands = sorted(_COMMANDS.keys())
|
| + for command in commands:
|
| + cmd = _COMMANDS[command]
|
| + if not cmd.hidden:
|
| + print command
|
| +
|
| +
|
| +# deprecated stuff #############################################################
|
| +
|
| +_COMMANDS = CommandLine()
|
| +
|
| +DEFAULT_COPYRIGHT = '''\
|
| +Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
|
| +http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
|
| +
|
| +@deprecated('use cls.register(cli)')
|
| +def register_commands(commands):
|
| + """register existing commands"""
|
| + for command_klass in commands:
|
| + _COMMANDS.register(command_klass)
|
| +
|
| +@deprecated('use args.pop(0)')
|
| +def main_run(args, doc=None, copyright=None, version=None):
|
| + """command line tool: run command specified by argument list (without the
|
| + program name). Raise SystemExit with status 0 if everything went fine.
|
| +
|
| + >>> main_run(sys.argv[1:])
|
| + """
|
| + _COMMANDS.doc = doc
|
| + _COMMANDS.copyright = copyright
|
| + _COMMANDS.version = version
|
| + _COMMANDS.run(args)
|
| +
|
| +@deprecated('use args.pop(0)')
|
| +def pop_arg(args_list, expected_size_after=None, msg="Missing argument"):
|
| + """helper function to get and check command line arguments"""
|
| + try:
|
| + value = args_list.pop(0)
|
| + except IndexError:
|
| + raise BadCommandUsage(msg)
|
| + if expected_size_after is not None and len(args_list) > expected_size_after:
|
| + raise BadCommandUsage('too many arguments')
|
| + return value
|
| +
|
|
|