OLD | NEW |
(Empty) | |
| 1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
| 2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 3 # |
| 4 # This file is part of logilab-common. |
| 5 # |
| 6 # logilab-common is free software: you can redistribute it and/or modify it unde
r |
| 7 # the terms of the GNU Lesser General Public License as published by the Free |
| 8 # Software Foundation, either version 2.1 of the License, or (at your option) an
y |
| 9 # later version. |
| 10 # |
| 11 # logilab-common is distributed in the hope that it will be useful, but WITHOUT |
| 12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
| 14 # details. |
| 15 # |
| 16 # You should have received a copy of the GNU Lesser General Public License along |
| 17 # with logilab-common. If not, see <http://www.gnu.org/licenses/>. |
| 18 """Helper functions to support command line tools providing more than |
| 19 one command. |
| 20 |
| 21 e.g called as "tool command [options] args..." where <options> and <args> are |
| 22 command'specific |
| 23 """ |
| 24 |
| 25 from __future__ import print_function |
| 26 |
| 27 __docformat__ = "restructuredtext en" |
| 28 |
| 29 import sys |
| 30 import logging |
| 31 from os.path import basename |
| 32 |
| 33 from logilab.common.configuration import Configuration |
| 34 from logilab.common.logging_ext import init_log, get_threshold |
| 35 from logilab.common.deprecation import deprecated |
| 36 |
| 37 |
| 38 class BadCommandUsage(Exception): |
| 39 """Raised when an unknown command is used or when a command is not |
| 40 correctly used (bad options, too much / missing arguments...). |
| 41 |
| 42 Trigger display of command usage. |
| 43 """ |
| 44 |
| 45 class CommandError(Exception): |
| 46 """Raised when a command can't be processed and we want to display it and |
| 47 exit, without traceback nor usage displayed. |
| 48 """ |
| 49 |
| 50 |
| 51 # command line access point #################################################### |
| 52 |
| 53 class CommandLine(dict): |
| 54 """Usage: |
| 55 |
| 56 >>> LDI = cli.CommandLine('ldi', doc='Logilab debian installer', |
| 57 version=version, rcfile=RCFILE) |
| 58 >>> LDI.register(MyCommandClass) |
| 59 >>> LDI.register(MyOtherCommandClass) |
| 60 >>> LDI.run(sys.argv[1:]) |
| 61 |
| 62 Arguments: |
| 63 |
| 64 * `pgm`, the program name, default to `basename(sys.argv[0])` |
| 65 |
| 66 * `doc`, a short description of the command line tool |
| 67 |
| 68 * `copyright`, additional doc string that will be appended to the generated |
| 69 doc |
| 70 |
| 71 * `version`, version number of string of the tool. If specified, global |
| 72 --version option will be available. |
| 73 |
| 74 * `rcfile`, path to a configuration file. If specified, global --C/--rc-file |
| 75 option will be available? self.rcfile = rcfile |
| 76 |
| 77 * `logger`, logger to propagate to commands, default to |
| 78 `logging.getLogger(self.pgm))` |
| 79 """ |
| 80 def __init__(self, pgm=None, doc=None, copyright=None, version=None, |
| 81 rcfile=None, logthreshold=logging.ERROR, |
| 82 check_duplicated_command=True): |
| 83 if pgm is None: |
| 84 pgm = basename(sys.argv[0]) |
| 85 self.pgm = pgm |
| 86 self.doc = doc |
| 87 self.copyright = copyright |
| 88 self.version = version |
| 89 self.rcfile = rcfile |
| 90 self.logger = None |
| 91 self.logthreshold = logthreshold |
| 92 self.check_duplicated_command = check_duplicated_command |
| 93 |
| 94 def register(self, cls, force=False): |
| 95 """register the given :class:`Command` subclass""" |
| 96 assert not self.check_duplicated_command or force or not cls.name in sel
f, \ |
| 97 'a command %s is already defined' % cls.name |
| 98 self[cls.name] = cls |
| 99 return cls |
| 100 |
| 101 def run(self, args): |
| 102 """main command line access point: |
| 103 * init logging |
| 104 * handle global options (-h/--help, --version, -C/--rc-file) |
| 105 * check command |
| 106 * run command |
| 107 |
| 108 Terminate by :exc:`SystemExit` |
| 109 """ |
| 110 init_log(debug=True, # so that we use StreamHandler |
| 111 logthreshold=self.logthreshold, |
| 112 logformat='%(levelname)s: %(message)s') |
| 113 try: |
| 114 arg = args.pop(0) |
| 115 except IndexError: |
| 116 self.usage_and_exit(1) |
| 117 if arg in ('-h', '--help'): |
| 118 self.usage_and_exit(0) |
| 119 if self.version is not None and arg in ('--version'): |
| 120 print(self.version) |
| 121 sys.exit(0) |
| 122 rcfile = self.rcfile |
| 123 if rcfile is not None and arg in ('-C', '--rc-file'): |
| 124 try: |
| 125 rcfile = args.pop(0) |
| 126 arg = args.pop(0) |
| 127 except IndexError: |
| 128 self.usage_and_exit(1) |
| 129 try: |
| 130 command = self.get_command(arg) |
| 131 except KeyError: |
| 132 print('ERROR: no %s command' % arg) |
| 133 print() |
| 134 self.usage_and_exit(1) |
| 135 try: |
| 136 sys.exit(command.main_run(args, rcfile)) |
| 137 except KeyboardInterrupt as exc: |
| 138 print('Interrupted', end=' ') |
| 139 if str(exc): |
| 140 print(': %s' % exc, end=' ') |
| 141 print() |
| 142 sys.exit(4) |
| 143 except BadCommandUsage as err: |
| 144 print('ERROR:', err) |
| 145 print() |
| 146 print(command.help()) |
| 147 sys.exit(1) |
| 148 |
| 149 def create_logger(self, handler, logthreshold=None): |
| 150 logger = logging.Logger(self.pgm) |
| 151 logger.handlers = [handler] |
| 152 if logthreshold is None: |
| 153 logthreshold = get_threshold(self.logthreshold) |
| 154 logger.setLevel(logthreshold) |
| 155 return logger |
| 156 |
| 157 def get_command(self, cmd, logger=None): |
| 158 if logger is None: |
| 159 logger = self.logger |
| 160 if logger is None: |
| 161 logger = self.logger = logging.getLogger(self.pgm) |
| 162 logger.setLevel(get_threshold(self.logthreshold)) |
| 163 return self[cmd](logger) |
| 164 |
| 165 def usage(self): |
| 166 """display usage for the main program (i.e. when no command supplied) |
| 167 and exit |
| 168 """ |
| 169 print('usage:', self.pgm, end=' ') |
| 170 if self.rcfile: |
| 171 print('[--rc-file=<configuration file>]', end=' ') |
| 172 print('<command> [options] <command argument>...') |
| 173 if self.doc: |
| 174 print('\n%s' % self.doc) |
| 175 print(''' |
| 176 Type "%(pgm)s <command> --help" for more information about a specific |
| 177 command. Available commands are :\n''' % self.__dict__) |
| 178 max_len = max([len(cmd) for cmd in self]) |
| 179 padding = ' ' * max_len |
| 180 for cmdname, cmd in sorted(self.items()): |
| 181 if not cmd.hidden: |
| 182 print(' ', (cmdname + padding)[:max_len], cmd.short_description(
)) |
| 183 if self.rcfile: |
| 184 print(''' |
| 185 Use --rc-file=<configuration file> / -C <configuration file> before the command |
| 186 to specify a configuration file. Default to %s. |
| 187 ''' % self.rcfile) |
| 188 print('''%(pgm)s -h/--help |
| 189 display this usage information and exit''' % self.__dict__) |
| 190 if self.version: |
| 191 print('''%(pgm)s -v/--version |
| 192 display version configuration and exit''' % self.__dict__) |
| 193 if self.copyright: |
| 194 print('\n', self.copyright) |
| 195 |
| 196 def usage_and_exit(self, status): |
| 197 self.usage() |
| 198 sys.exit(status) |
| 199 |
| 200 |
| 201 # base command classes ######################################################### |
| 202 |
| 203 class Command(Configuration): |
| 204 """Base class for command line commands. |
| 205 |
| 206 Class attributes: |
| 207 |
| 208 * `name`, the name of the command |
| 209 |
| 210 * `min_args`, minimum number of arguments, None if unspecified |
| 211 |
| 212 * `max_args`, maximum number of arguments, None if unspecified |
| 213 |
| 214 * `arguments`, string describing arguments, used in command usage |
| 215 |
| 216 * `hidden`, boolean flag telling if the command should be hidden, e.g. does |
| 217 not appear in help's commands list |
| 218 |
| 219 * `options`, options list, as allowed by :mod:configuration |
| 220 """ |
| 221 |
| 222 arguments = '' |
| 223 name = '' |
| 224 # hidden from help ? |
| 225 hidden = False |
| 226 # max/min args, None meaning unspecified |
| 227 min_args = None |
| 228 max_args = None |
| 229 |
| 230 @classmethod |
| 231 def description(cls): |
| 232 return cls.__doc__.replace(' ', '') |
| 233 |
| 234 @classmethod |
| 235 def short_description(cls): |
| 236 return cls.description().split('.')[0] |
| 237 |
| 238 def __init__(self, logger): |
| 239 usage = '%%prog %s %s\n\n%s' % (self.name, self.arguments, |
| 240 self.description()) |
| 241 Configuration.__init__(self, usage=usage) |
| 242 self.logger = logger |
| 243 |
| 244 def check_args(self, args): |
| 245 """check command's arguments are provided""" |
| 246 if self.min_args is not None and len(args) < self.min_args: |
| 247 raise BadCommandUsage('missing argument') |
| 248 if self.max_args is not None and len(args) > self.max_args: |
| 249 raise BadCommandUsage('too many arguments') |
| 250 |
| 251 def main_run(self, args, rcfile=None): |
| 252 """Run the command and return status 0 if everything went fine. |
| 253 |
| 254 If :exc:`CommandError` is raised by the underlying command, simply log |
| 255 the error and return status 2. |
| 256 |
| 257 Any other exceptions, including :exc:`BadCommandUsage` will be |
| 258 propagated. |
| 259 """ |
| 260 if rcfile: |
| 261 self.load_file_configuration(rcfile) |
| 262 args = self.load_command_line_configuration(args) |
| 263 try: |
| 264 self.check_args(args) |
| 265 self.run(args) |
| 266 except CommandError as err: |
| 267 self.logger.error(err) |
| 268 return 2 |
| 269 return 0 |
| 270 |
| 271 def run(self, args): |
| 272 """run the command with its specific arguments""" |
| 273 raise NotImplementedError() |
| 274 |
| 275 |
| 276 class ListCommandsCommand(Command): |
| 277 """list available commands, useful for bash completion.""" |
| 278 name = 'listcommands' |
| 279 arguments = '[command]' |
| 280 hidden = True |
| 281 |
| 282 def run(self, args): |
| 283 """run the command with its specific arguments""" |
| 284 if args: |
| 285 command = args.pop() |
| 286 cmd = _COMMANDS[command] |
| 287 for optname, optdict in cmd.options: |
| 288 print('--help') |
| 289 print('--' + optname) |
| 290 else: |
| 291 commands = sorted(_COMMANDS.keys()) |
| 292 for command in commands: |
| 293 cmd = _COMMANDS[command] |
| 294 if not cmd.hidden: |
| 295 print(command) |
| 296 |
| 297 |
| 298 # deprecated stuff ############################################################# |
| 299 |
| 300 _COMMANDS = CommandLine() |
| 301 |
| 302 DEFAULT_COPYRIGHT = '''\ |
| 303 Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
| 304 http://www.logilab.fr/ -- mailto:contact@logilab.fr''' |
| 305 |
| 306 @deprecated('use cls.register(cli)') |
| 307 def register_commands(commands): |
| 308 """register existing commands""" |
| 309 for command_klass in commands: |
| 310 _COMMANDS.register(command_klass) |
| 311 |
| 312 @deprecated('use args.pop(0)') |
| 313 def main_run(args, doc=None, copyright=None, version=None): |
| 314 """command line tool: run command specified by argument list (without the |
| 315 program name). Raise SystemExit with status 0 if everything went fine. |
| 316 |
| 317 >>> main_run(sys.argv[1:]) |
| 318 """ |
| 319 _COMMANDS.doc = doc |
| 320 _COMMANDS.copyright = copyright |
| 321 _COMMANDS.version = version |
| 322 _COMMANDS.run(args) |
| 323 |
| 324 @deprecated('use args.pop(0)') |
| 325 def pop_arg(args_list, expected_size_after=None, msg="Missing argument"): |
| 326 """helper function to get and check command line arguments""" |
| 327 try: |
| 328 value = args_list.pop(0) |
| 329 except IndexError: |
| 330 raise BadCommandUsage(msg) |
| 331 if expected_size_after is not None and len(args_list) > expected_size_after: |
| 332 raise BadCommandUsage('too many arguments') |
| 333 return value |
| 334 |
OLD | NEW |