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