Index: third_party/logilab/logilab/common/clcommands.py |
diff --git a/third_party/logilab/logilab/common/clcommands.py b/third_party/logilab/logilab/common/clcommands.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4778b99b024e0f2eb816d64f34be6b8f46542d80 |
--- /dev/null |
+++ b/third_party/logilab/logilab/common/clcommands.py |
@@ -0,0 +1,334 @@ |
+# 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 |
+""" |
+ |
+from __future__ import print_function |
+ |
+__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 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''' |
+ |
+@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 |
+ |