Package flumotion :: Package extern :: Package command :: Package command :: Module command
[hide private]

Source Code for Module flumotion.extern.command.command.command

  1  # -*- Mode: Python; test-case-name: test_command -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3   
  4  # This file is released under the standard PSF license. 
  5   
  6  """ 
  7  Command class. 
  8  """ 
  9   
 10  import optparse 
 11  import sys 
 12   
 13   
14 -class CommandHelpFormatter(optparse.IndentedHelpFormatter):
15 """ 16 I format the description as usual, but add an overview of commands 17 after it if there are any, formatted like the options. 18 """ 19 20 _commands = None 21 _aliases = None 22
23 - def addCommand(self, name, description):
24 if self._commands is None: 25 self._commands = {} 26 self._commands[name] = description
27
28 - def addAlias(self, alias):
29 if self._aliases is None: 30 self._aliases = [] 31 self._aliases.append(alias)
32 33 ### override parent method 34
35 - def format_description(self, description, width=None):
36 # textwrap doesn't allow for a way to preserve double newlines 37 # to separate paragraphs, so we do it here. 38 paragraphs = description.split('\n\n') 39 rets = [] 40 41 for paragraph in paragraphs: 42 # newlines starting with a space/dash are treated as a table, ie as 43 # is 44 lines = paragraph.split('\n -') 45 formatted = [] 46 for line in lines: 47 formatted.append( 48 optparse.IndentedHelpFormatter.format_description( 49 self, line)) 50 rets.append(" -".join(formatted)) 51 52 ret = "\n".join(rets) 53 54 # add aliases 55 if self._aliases: 56 ret += "\nAliases: " + ", ".join(self._aliases) + "\n" 57 58 # add subcommands 59 if self._commands: 60 commandDesc = [] 61 commandDesc.append("Commands:") 62 keys = self._commands.keys() 63 keys.sort() 64 length = 0 65 for key in keys: 66 if len(key) > length: 67 length = len(key) 68 for name in keys: 69 formatString = " %-" + "%d" % length + "s %s" 70 commandDesc.append(formatString % (name, self._commands[name])) 71 ret += "\n" + "\n".join(commandDesc) + "\n" 72 return ret
73 74
75 -class CommandOptionParser(optparse.OptionParser):
76 """ 77 I parse options as usual, but I explicitly allow setting stdout 78 so that our print_help() method (invoked by default with -h/--help) 79 defaults to writing there. 80 81 I also override exit() so that I can be used in interactive shells. 82 83 @ivar help_printed: whether help was printed during parsing 84 @ivar usage_printed: whether usage was printed during parsing 85 """ 86 help_printed = False 87 usage_printed = False 88 89 _stdout = sys.stdout 90
91 - def set_stdout(self, stdout):
92 self._stdout = stdout
93
94 - def parse_args(self, args=None, values=None):
95 self.help_printed = False 96 self.usage_printed = False 97 return optparse.OptionParser.parse_args(self, args, values)
98 # we're overriding the built-in file, but we need to since this is 99 # the signature from the base class 100 __pychecker__ = 'no-shadowbuiltin' 101
102 - def print_help(self, file=None):
103 # we are overriding a parent method so we can't do anything about file 104 __pychecker__ = 'no-shadowbuiltin' 105 if file is None: 106 file = self._stdout 107 file.write(self.format_help()) 108 self.help_printed = True
109
110 - def print_usage(self, file=None):
111 optparse.OptionParser.print_usage(self, file) 112 self.usage_printed = True
113
114 - def exit(self, status=0, msg=None):
115 if msg: 116 sys.stderr.write(msg) 117 118 return status
119 120
121 -class Command(object):
122 """ 123 I am a class that handles a command for a program. 124 Commands can be nested underneath a command for further processing. 125 126 @cvar name: name of the command, lowercase; 127 defaults to the lowercase version of the class name 128 @cvar aliases: list of alternative lowercase names recognized 129 @type aliases: list of str 130 @cvar usage: short one-line usage string; 131 %command gets expanded to a sub-command or [commands] 132 as appropriate. Don't specify the command name itself, 133 it will be added automatically. If not set, defaults 134 to name. 135 @cvar summary: short one-line summary of the command 136 @cvar description: longer paragraph explaining the command 137 @cvar subCommands: dict of name -> commands below this command 138 @type subCommands: dict of str -> L{Command} 139 @cvar parser: the option parser used for parsing 140 @type parser: L{optparse.OptionParser} 141 """ 142 name = None 143 aliases = None 144 usage = None 145 summary = None 146 description = None 147 parentCommand = None 148 subCommands = None 149 subCommandClasses = None 150 aliasedSubCommands = None 151 parser = None 152
153 - def __init__(self, parentCommand=None, stdout=None, 154 stderr=None, width=None):
155 """ 156 Create a new command instance, with the given parent. 157 Allows for redirecting stdout and stderr if needed. 158 This redirection will be passed on to child commands. 159 """ 160 if not self.name: 161 self.name = self.__class__.__name__.lower() 162 self._stdout = stdout 163 self._stderr = stderr 164 self.parentCommand = parentCommand 165 166 # create subcommands if we have them 167 self.subCommands = {} 168 self.aliasedSubCommands = {} 169 if self.subCommandClasses: 170 for C in self.subCommandClasses: 171 c = C(self, stdout=stdout, stderr=stderr, width=width) 172 self.subCommands[c.name] = c 173 if c.aliases: 174 for alias in c.aliases: 175 self.aliasedSubCommands[alias] = c 176 177 # create our formatter and add subcommands if we have them 178 formatter = CommandHelpFormatter(width=width) 179 if self.subCommands: 180 if not self.description: 181 if self.summary: 182 self.description = self.summary 183 else: 184 raise AttributeError, \ 185 "%r needs a summary or description " \ 186 "for help formatting" % self 187 188 for name, command in self.subCommands.items(): 189 formatter.addCommand(name, command.summary or 190 command.description) 191 192 if self.aliases: 193 for alias in self.aliases: 194 formatter.addAlias(alias) 195 196 # expand %command for the bottom usage 197 usage = self.usage or '' 198 if not usage: 199 # if no usage, but subcommands, then default to showing that 200 if self.subCommands: 201 usage = "%command" 202 203 # the main program name shouldn't get prepended, because %prog 204 # already expands to the name 205 if not usage.startswith('%prog'): 206 usage = self.name + ' ' + usage 207 208 usages = [usage, ] 209 if usage.find("%command") > -1: 210 if self.subCommands: 211 usage = usage.split("%command")[0] + '[command]' 212 usages = [usage, ] 213 else: 214 # %command used in a leaf command 215 usages = usage.split("%command") 216 usages.reverse() 217 218 # FIXME: abstract this into getUsage that takes an optional 219 # parentCommand on where to stop recursing up 220 # useful for implementing subshells 221 222 # walk the tree up for our usage 223 c = self.parentCommand 224 while c: 225 usage = c.usage or c.name 226 if usage.find(" %command") > -1: 227 usage = usage.split(" %command")[0] 228 usages.append(usage) 229 c = c.parentCommand 230 usages.reverse() 231 usage = " ".join(usages) 232 233 # create our parser 234 description = self.description or self.summary 235 if description: 236 description = description.strip() 237 self.parser = CommandOptionParser( 238 usage=usage, description=description, 239 formatter=formatter) 240 self.parser.set_stdout(self.stdout) 241 self.parser.disable_interspersed_args() 242 243 # allow subclasses to add options 244 self.addOptions()
245
246 - def addOptions(self):
247 """ 248 Override me to add options to the parser. 249 """ 250 pass
251
252 - def do(self, args):
253 """ 254 Override me to implement the functionality of the command. 255 256 @rtype: int 257 @returns: an exit code, or None if no actual action was taken. 258 """ 259 raise NotImplementedError('Implement %s.do()' % self.__class__) 260 # by default, return 1 and hopefully show help 261 return 1
262
263 - def parse(self, argv):
264 """ 265 Parse the given arguments and act on them. 266 267 @param argv: list of arguments to parse 268 @type argv: list of unicode 269 270 @rtype: int 271 @returns: an exit code, or None if no actual action was taken. 272 """ 273 # note: no arguments should be passed as an empty list, not a list 274 # with an empty str as ''.split(' ') returns 275 self.debug('calling %r.parse_args' % self) 276 self.options, args = self.parser.parse_args(argv) 277 self.debug('called %r.parse_args' % self) 278 279 # if we were asked to print help or usage, we are done 280 if self.parser.usage_printed or self.parser.help_printed: 281 return None 282 283 # FIXME: make handleOptions not take options, since we store it 284 # in self.options now 285 ret = self.handleOptions(self.options) 286 if ret: 287 return ret 288 289 # handle pleas for help 290 if args and args[0] == 'help': 291 self.debug('Asked for help, args %r' % args) 292 293 # give help on current command if only 'help' is passed 294 if len(args) == 1: 295 self.outputHelp() 296 return 0 297 298 # complain if we were asked for help on a subcommand, but we don't 299 # have any 300 if not self.subCommands: 301 self.stderr.write('No subcommands defined.\n') 302 self.parser.print_usage(file=self.stderr) 303 self.stderr.write( 304 "Use --help to get more information about this command.\n") 305 return 1 306 307 # rewrite the args the other way around; 308 # help doap becomes doap help so it gets deferred to the doap 309 # command 310 args = [args[1], args[0]] 311 312 # if we don't have args or don't have subcommands, 313 # defer to our do() method 314 # allows implementing a do() for commands that also have subcommands 315 if not args or not self.subCommands: 316 self.debug('no args or no subcommands, doing') 317 try: 318 ret = self.do(args) 319 except CommandOk, e: 320 ret = e.status 321 self.stdout.write(e.output + '\n') 322 except CommandExited, e: 323 ret = e.status 324 self.stderr.write(e.output + '\n') 325 except NotImplementedError: 326 self.parser.print_usage(file=self.stderr) 327 self.stderr.write( 328 "Use --help to get a list of commands.\n") 329 return 1 330 331 # if everything's fine, we return 0 332 if not ret: 333 ret = 0 334 335 return ret 336 337 # if we do have subcommands, defer to them 338 try: 339 command = args[0] 340 except IndexError: 341 self.parser.print_usage(file=self.stderr) 342 self.stderr.write( 343 "Use --help to get a list of commands.\n") 344 return 1 345 346 # FIXME: check users and enable this 347 # assert type(command) is unicode 348 if command in self.subCommands.keys(): 349 return self.subCommands[command].parse(args[1:]) 350 351 if self.aliasedSubCommands: 352 if command in self.aliasedSubCommands.keys(): 353 return self.aliasedSubCommands[command].parse(args[1:]) 354 355 self.stderr.write("Unknown command '%s'.\n" % command.encode('utf-8')) 356 self.parser.print_usage(file=self.stderr) 357 return 1
358
359 - def handleOptions(self, options):
360 """ 361 Handle the parsed options. 362 """ 363 pass
364
365 - def outputHelp(self, file=None):
366 """ 367 Output help information. 368 """ 369 __pychecker__ = 'no-shadowbuiltin' 370 self.debug('outputHelp') 371 if not file: 372 file = self.stderr 373 self.parser.print_help(file=file)
374
375 - def outputUsage(self, file=None):
376 """ 377 Output usage information. 378 Used when the options or arguments were missing or wrong. 379 """ 380 __pychecker__ = 'no-shadowbuiltin' 381 self.debug('outputUsage') 382 if not file: 383 file = self.stderr 384 self.parser.print_usage(file=file)
385
386 - def getRootCommand(self):
387 """ 388 Return the top-level command, which is typically the program. 389 """ 390 c = self 391 while c.parentCommand: 392 c = c.parentCommand 393 return c
394
395 - def debug(self, format, *args):
396 """ 397 Override me to handle debug output from this class. 398 """ 399 pass
400
401 - def getFullName(self):
402 names = [] 403 c = self 404 while c: 405 names.append(c.name) 406 c = c.parentCommand 407 names.reverse() 408 return " ".join(names)
409
410 - def _getStdout(self):
411 # if set explicitly, use it 412 if self._stdout: 413 return self._stdout 414 415 # if I am the root command, default 416 if not self.parentCommand: 417 return sys.stdout 418 419 # otherwise delegate to my parent 420 return self.parentCommand.stdout
421 422 stdout = property(_getStdout) 423 # FIXME: do we want a separate one ? 424 stderr = property(_getStdout)
425 426
427 -class CommandExited(Exception):
428
429 - def __init__(self, status, output):
430 self.args = (status, output) 431 self.status = status 432 self.output = output
433 434
435 -class CommandOk(CommandExited):
436
437 - def __init__(self, output):
438 CommandExited.__init__(self, 0, output)
439 440
441 -class CommandError(CommandExited):
442
443 - def __init__(self, output):
444 CommandExited.__init__(self, 3, output)
445 446
447 -def commandToCmdClass(command):
448 """ 449 @type command: L{Command} 450 451 Take a Command instance and create a L{cmd.Cmd} class from it that 452 implements a command line interpreter, using the commands under the given 453 Command instance as its subcommands. 454 455 Example use in a command: 456 457 >>> def do(self, args): 458 ... cmd = command.commandToCmdClass(self)() 459 ... cmd.prompt = 'prompt> ' 460 ... while not cmd.exited: 461 ... cmd.cmdloop() 462 463 @rtype: L{cmd.Cmd} 464 """ 465 import cmd 466 467 # internal class to subclass cmd.Cmd with a Ctrl-D handler 468 469 class _CommandWrappingCmd(cmd.Cmd): 470 prompt = '(command) ' 471 exited = False 472 command = None # the original Command subclass 473 474 def __repr__(self): 475 return "<_CommandWrappingCmd for Command %r>" % self.command
476 477 def do_EOF(self, args): 478 self.stdout.write('\n') 479 self.exited = True 480 sys.exit(0) 481 482 def do_exit(self, args): 483 self.exited = True 484 sys.exit(0) 485 486 def help_EOF(self): 487 print 'Exit.' 488 489 def help_exit(self): 490 print 'Exit.' 491 492 # populate the Cmd interpreter from our command class 493 cmdClass = _CommandWrappingCmd 494 cmdClass.command = command 495 496 for name, subCommand in command.subCommands.items() \ 497 + command.aliasedSubCommands.items(): 498 if name == 'shell': 499 continue 500 command.debug('Adding shell command %s for %r' % (name, subCommand)) 501 502 # add do command 503 methodName = 'do_' + name 504 505 def generateDo(c): 506 507 def do_(s, line): 508 # line is coming from a terminal; usually it is a utf-8 encoded 509 # string. 510 # Instead of making every Command subclass implement do with 511 # unicode decoding, we do it here. 512 line = line.decode('utf-8') 513 # the do_ method is passed a single argument consisting of 514 # the remainder of the line 515 args = line.split(' ') 516 command.debug('Asking %r to parse %r' % (c, args)) 517 return c.parse(args) 518 return do_ 519 520 method = generateDo(subCommand) 521 setattr(cmdClass, methodName, method) 522 523 524 # add help command 525 methodName = 'help_' + name 526 527 def generateHelp(c): 528 529 def help_(s): 530 command.parser.print_help(file=s.stdout) 531 return help_ 532 533 method = generateHelp(subCommand) 534 setattr(cmdClass, methodName, method) 535 536 return cmdClass 537 538
539 -def commandToCmd(command):
540 # for compatibility reasons 541 return commandToCmdClass(command)()
542