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

Source Code for Module 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 return cls
98
99 - def run(self, args):
100 """main command line access point: 101 * init logging 102 * handle global options (-h/--help, --version, -C/--rc-file) 103 * check command 104 * run command 105 106 Terminate by :exc:`SystemExit` 107 """ 108 init_log(debug=True, # so that we use StreamHandler 109 logthreshold=self.logthreshold, 110 logformat='%(levelname)s: %(message)s') 111 try: 112 arg = args.pop(0) 113 except IndexError: 114 self.usage_and_exit(1) 115 if arg in ('-h', '--help'): 116 self.usage_and_exit(0) 117 if self.version is not None and arg in ('--version'): 118 print(self.version) 119 sys.exit(0) 120 rcfile = self.rcfile 121 if rcfile is not None and arg in ('-C', '--rc-file'): 122 try: 123 rcfile = args.pop(0) 124 arg = args.pop(0) 125 except IndexError: 126 self.usage_and_exit(1) 127 try: 128 command = self.get_command(arg) 129 except KeyError: 130 print('ERROR: no %s command' % arg) 131 print() 132 self.usage_and_exit(1) 133 try: 134 sys.exit(command.main_run(args, rcfile)) 135 except KeyboardInterrupt as exc: 136 print('Interrupted', end=' ') 137 if str(exc): 138 print(': %s' % exc, end=' ') 139 print() 140 sys.exit(4) 141 except BadCommandUsage as err: 142 print('ERROR:', err) 143 print() 144 print(command.help()) 145 sys.exit(1)
146
147 - def create_logger(self, handler, logthreshold=None):
148 logger = logging.Logger(self.pgm) 149 logger.handlers = [handler] 150 if logthreshold is None: 151 logthreshold = get_threshold(self.logthreshold) 152 logger.setLevel(logthreshold) 153 return logger
154
155 - def get_command(self, cmd, logger=None):
156 if logger is None: 157 logger = self.logger 158 if logger is None: 159 logger = self.logger = logging.getLogger(self.pgm) 160 logger.setLevel(get_threshold(self.logthreshold)) 161 return self[cmd](logger)
162
163 - def usage(self):
164 """display usage for the main program (i.e. when no command supplied) 165 and exit 166 """ 167 print('usage:', self.pgm, end=' ') 168 if self.rcfile: 169 print('[--rc-file=<configuration file>]', end=' ') 170 print('<command> [options] <command argument>...') 171 if self.doc: 172 print('\n%s' % self.doc) 173 print(''' 174 Type "%(pgm)s <command> --help" for more information about a specific 175 command. Available commands are :\n''' % self.__dict__) 176 max_len = max([len(cmd) for cmd in self]) 177 padding = ' ' * max_len 178 for cmdname, cmd in sorted(self.items()): 179 if not cmd.hidden: 180 print(' ', (cmdname + padding)[:max_len], cmd.short_description()) 181 if self.rcfile: 182 print(''' 183 Use --rc-file=<configuration file> / -C <configuration file> before the command 184 to specify a configuration file. Default to %s. 185 ''' % self.rcfile) 186 print('''%(pgm)s -h/--help 187 display this usage information and exit''' % self.__dict__) 188 if self.version: 189 print('''%(pgm)s -v/--version 190 display version configuration and exit''' % self.__dict__) 191 if self.copyright: 192 print('\n', self.copyright)
193
194 - def usage_and_exit(self, status):
195 self.usage() 196 sys.exit(status)
197
198 199 # base command classes ######################################################### 200 201 -class Command(Configuration):
202 """Base class for command line commands. 203 204 Class attributes: 205 206 * `name`, the name of the command 207 208 * `min_args`, minimum number of arguments, None if unspecified 209 210 * `max_args`, maximum number of arguments, None if unspecified 211 212 * `arguments`, string describing arguments, used in command usage 213 214 * `hidden`, boolean flag telling if the command should be hidden, e.g. does 215 not appear in help's commands list 216 217 * `options`, options list, as allowed by :mod:configuration 218 """ 219 220 arguments = '' 221 name = '' 222 # hidden from help ? 223 hidden = False 224 # max/min args, None meaning unspecified 225 min_args = None 226 max_args = None 227 228 @classmethod
229 - def description(cls):
230 return cls.__doc__.replace(' ', '')
231 232 @classmethod
233 - def short_description(cls):
234 return cls.description().split('.')[0]
235
236 - def __init__(self, logger):
237 usage = '%%prog %s %s\n\n%s' % (self.name, self.arguments, 238 self.description()) 239 Configuration.__init__(self, usage=usage) 240 self.logger = logger
241
242 - def check_args(self, args):
243 """check command's arguments are provided""" 244 if self.min_args is not None and len(args) < self.min_args: 245 raise BadCommandUsage('missing argument') 246 if self.max_args is not None and len(args) > self.max_args: 247 raise BadCommandUsage('too many arguments')
248
249 - def main_run(self, args, rcfile=None):
250 """Run the command and return status 0 if everything went fine. 251 252 If :exc:`CommandError` is raised by the underlying command, simply log 253 the error and return status 2. 254 255 Any other exceptions, including :exc:`BadCommandUsage` will be 256 propagated. 257 """ 258 if rcfile: 259 self.load_file_configuration(rcfile) 260 args = self.load_command_line_configuration(args) 261 try: 262 self.check_args(args) 263 self.run(args) 264 except CommandError as err: 265 self.logger.error(err) 266 return 2 267 return 0
268
269 - def run(self, args):
270 """run the command with its specific arguments""" 271 raise NotImplementedError()
272
273 274 -class ListCommandsCommand(Command):
275 """list available commands, useful for bash completion.""" 276 name = 'listcommands' 277 arguments = '[command]' 278 hidden = True 279
280 - def run(self, args):
281 """run the command with its specific arguments""" 282 if args: 283 command = args.pop() 284 cmd = _COMMANDS[command] 285 for optname, optdict in cmd.options: 286 print('--help') 287 print('--' + optname) 288 else: 289 commands = sorted(_COMMANDS.keys()) 290 for command in commands: 291 cmd = _COMMANDS[command] 292 if not cmd.hidden: 293 print(command)
294 295 296 # deprecated stuff ############################################################# 297 298 _COMMANDS = CommandLine() 299 300 DEFAULT_COPYRIGHT = '''\ 301 Copyright (c) 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved. 302 http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
303 304 @deprecated('use cls.register(cli)') 305 -def register_commands(commands):
306 """register existing commands""" 307 for command_klass in commands: 308 _COMMANDS.register(command_klass)
309
310 @deprecated('use args.pop(0)') 311 -def main_run(args, doc=None, copyright=None, version=None):
312 """command line tool: run command specified by argument list (without the 313 program name). Raise SystemExit with status 0 if everything went fine. 314 315 >>> main_run(sys.argv[1:]) 316 """ 317 _COMMANDS.doc = doc 318 _COMMANDS.copyright = copyright 319 _COMMANDS.version = version 320 _COMMANDS.run(args)
321
322 @deprecated('use args.pop(0)') 323 -def pop_arg(args_list, expected_size_after=None, msg="Missing argument"):
324 """helper function to get and check command line arguments""" 325 try: 326 value = args_list.pop(0) 327 except IndexError: 328 raise BadCommandUsage(msg) 329 if expected_size_after is not None and len(args_list) > expected_size_after: 330 raise BadCommandUsage('too many arguments') 331 return value
332