# Source code for logilab.common.clcommands

    # 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 callable_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 cls.name not 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 as exc:
                print("Interrupted", end=" ")
                if str(exc):
                    print(": %s" % exc, end=" ")
                print()
                sys.exit(4)
            except BadCommandUsage as 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, end=" ")
            if self.rcfile:
                print("[--rc-file=<configuration file>]", end=" ")
            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 as 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"""

    @callable_deprecated("use cls.register(cli)")
    def register_commands(commands):
        """register existing commands"""
        for command_klass in commands:
            _COMMANDS.register(command_klass)

    @callable_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)

    @callable_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