view clcommands.py @ 1491:8724116a446a

fix debian rev
author Sylvain Thénault <sylvain.thenault@logilab.fr>
date Tue, 17 Jul 2012 17:10:51 +0200
parents b0e29b5dc97c
children
line wrap: on
line source
# 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