Package logilab :: Package common :: Module clcommands
[frames] | no frames]

Source Code for Module logilab.common.clcommands

  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 under 
  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) any 
  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 self, \ 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