| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- # 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
|