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

Source Code for Module flumotion.extern.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 if usage.find("%command") > -1: 209 usage = usage.split("%command")[0] + '[command]' 210 usages = [usage, ] 211 212 # FIXME: abstract this into getUsage that takes an optional 213 # parentCommand on where to stop recursing up 214 # useful for implementing subshells 215 216 # walk the tree up for our usage 217 c = self.parentCommand 218 while c: 219 usage = c.usage or c.name 220 if usage.find(" %command") > -1: 221 usage = usage.split(" %command")[0] 222 usages.append(usage) 223 c = c.parentCommand 224 usages.reverse() 225 usage = " ".join(usages) 226 227 # create our parser 228 description = self.description or self.summary 229 if description: 230 description = description.strip() 231 self.parser = CommandOptionParser( 232 usage=usage, description=description, 233 formatter=formatter) 234 self.parser.set_stdout(self.stdout) 235 self.parser.disable_interspersed_args() 236 237 # allow subclasses to add options 238 self.addOptions()
239
240 - def addOptions(self):
241 """ 242 Override me to add options to the parser. 243 """ 244 pass
245
246 - def do(self, args):
247 """ 248 Override me to implement the functionality of the command. 249 250 @rtype: int 251 @returns: an exit code, or None if no actual action was taken. 252 """ 253 raise NotImplementedError('Implement %s.do()' % self.__class__) 254 # by default, return 1 and hopefully show help 255 return 1
256
257 - def parse(self, argv):
258 """ 259 Parse the given arguments and act on them. 260 261 @param argv: list of arguments to parse 262 @type argv: list of unicode 263 264 @rtype: int 265 @returns: an exit code, or None if no actual action was taken. 266 """ 267 # note: no arguments should be passed as an empty list, not a list 268 # with an empty str as ''.split(' ') returns 269 self.debug('calling %r.parse_args' % self) 270 self.options, args = self.parser.parse_args(argv) 271 self.debug('called %r.parse_args' % self) 272 273 # if we were asked to print help or usage, we are done 274 if self.parser.usage_printed or self.parser.help_printed: 275 return None 276 277 # FIXME: make handleOptions not take options, since we store it 278 # in self.options now 279 ret = self.handleOptions(self.options) 280 if ret: 281 return ret 282 283 # handle pleas for help 284 if args and args[0] == 'help': 285 self.debug('Asked for help, args %r' % args) 286 287 # give help on current command if only 'help' is passed 288 if len(args) == 1: 289 self.outputHelp() 290 return 0 291 292 # complain if we were asked for help on a subcommand, but we don't 293 # have any 294 if not self.subCommands: 295 self.stderr.write('No subcommands defined.\n') 296 self.parser.print_usage(file=self.stderr) 297 self.stderr.write( 298 "Use --help to get more information about this command.\n") 299 return 1 300 301 # rewrite the args the other way around; 302 # help doap becomes doap help so it gets deferred to the doap 303 # command 304 args = [args[1], args[0]] 305 306 # if we don't have args or don't have subcommands, 307 # defer to our do() method 308 # allows implementing a do() for commands that also have subcommands 309 if not args or not self.subCommands: 310 self.debug('no args or no subcommands, doing') 311 try: 312 ret = self.do(args) 313 except CommandOk, e: 314 ret = e.status 315 self.stdout.write(e.output + '\n') 316 except CommandExited, e: 317 ret = e.status 318 self.stderr.write(e.output + '\n') 319 except NotImplementedError: 320 self.parser.print_usage(file=self.stderr) 321 self.stderr.write( 322 "Use --help to get a list of commands.\n") 323 return 1 324 325 # if everything's fine, we return 0 326 if not ret: 327 ret = 0 328 329 return ret 330 331 # if we do have subcommands, defer to them 332 try: 333 command = args[0] 334 except IndexError: 335 self.parser.print_usage(file=self.stderr) 336 self.stderr.write( 337 "Use --help to get a list of commands.\n") 338 return 1 339 340 # FIXME: check users and enable this 341 # assert type(command) is unicode 342 if command in self.subCommands.keys(): 343 return self.subCommands[command].parse(args[1:]) 344 345 if self.aliasedSubCommands: 346 if command in self.aliasedSubCommands.keys(): 347 return self.aliasedSubCommands[command].parse(args[1:]) 348 349 self.stderr.write("Unknown command '%s'.\n" % command.encode('utf-8')) 350 self.parser.print_usage(file=self.stderr) 351 return 1
352
353 - def handleOptions(self, options):
354 """ 355 Handle the parsed options. 356 """ 357 pass
358
359 - def outputHelp(self, file=None):
360 """ 361 Output help information. 362 """ 363 __pychecker__ = 'no-shadowbuiltin' 364 self.debug('outputHelp') 365 if not file: 366 file = self.stderr 367 self.parser.print_help(file=file)
368
369 - def outputUsage(self, file=None):
370 """ 371 Output usage information. 372 Used when the options or arguments were missing or wrong. 373 """ 374 __pychecker__ = 'no-shadowbuiltin' 375 self.debug('outputUsage') 376 if not file: 377 file = self.stderr 378 self.parser.print_usage(file=file)
379
380 - def getRootCommand(self):
381 """ 382 Return the top-level command, which is typically the program. 383 """ 384 c = self 385 while c.parentCommand: 386 c = c.parentCommand 387 return c
388
389 - def debug(self, format, *args):
390 """ 391 Override me to handle debug output from this class. 392 """ 393 pass
394
395 - def getFullName(self):
396 names = [] 397 c = self 398 while c: 399 names.append(c.name) 400 c = c.parentCommand 401 names.reverse() 402 return " ".join(names)
403
404 - def _getStdout(self):
405 # if set explicitly, use it 406 if self._stdout: 407 return self._stdout 408 409 # if I am the root command, default 410 if not self.parentCommand: 411 return sys.stdout 412 413 # otherwise delegate to my parent 414 return self.parentCommand.stdout
415 416 stdout = property(_getStdout) 417 # FIXME: do we want a separate one ? 418 stderr = property(_getStdout)
419 420
421 -class CommandExited(Exception):
422
423 - def __init__(self, status, output):
424 self.args = (status, output) 425 self.status = status 426 self.output = output
427 428
429 -class CommandOk(CommandExited):
430
431 - def __init__(self, output):
432 CommandExited.__init__(self, 0, output)
433 434
435 -class CommandError(CommandExited):
436
437 - def __init__(self, output):
438 CommandExited.__init__(self, 3, output)
439 440
441 -def commandToCmdClass(command):
442 """ 443 @type command: L{Command} 444 445 Take a Command instance and create a L{cmd.Cmd} class from it that 446 implements a command line interpreter, using the commands under the given 447 Command instance as its subcommands. 448 449 Example use in a command: 450 451 >>> def do(self, args): 452 ... cmd = command.commandToCmdClass(self)() 453 ... cmd.prompt = 'prompt> ' 454 ... while not cmd.exited: 455 ... cmd.cmdloop() 456 457 @rtype: L{cmd.Cmd} 458 """ 459 import cmd 460 461 # internal class to subclass cmd.Cmd with a Ctrl-D handler 462 463 class _CommandWrappingCmd(cmd.Cmd): 464 prompt = '(command) ' 465 exited = False 466 command = None # the original Command subclass 467 468 def __repr__(self): 469 return "<_CommandWrappingCmd for Command %r>" % self.command
470 471 def do_EOF(self, args): 472 self.stdout.write('\n') 473 self.exited = True 474 sys.exit(0) 475 476 def do_exit(self, args): 477 self.exited = True 478 sys.exit(0) 479 480 def help_EOF(self): 481 print 'Exit.' 482 483 def help_exit(self): 484 print 'Exit.' 485 486 # populate the Cmd interpreter from our command class 487 cmdClass = _CommandWrappingCmd 488 cmdClass.command = command 489 490 for name, subCommand in command.subCommands.items() \ 491 + command.aliasedSubCommands.items(): 492 if name == 'shell': 493 continue 494 command.debug('Adding shell command %s for %r' % (name, subCommand)) 495 496 # add do command 497 methodName = 'do_' + name 498 499 def generateDo(c): 500 501 def do_(s, line): 502 # line is coming from a terminal; usually it is a utf-8 encoded 503 # string. 504 # Instead of making every Command subclass implement do with 505 # unicode decoding, we do it here. 506 line = line.decode('utf-8') 507 # the do_ method is passed a single argument consisting of 508 # the remainder of the line 509 args = line.split(' ') 510 command.debug('Asking %r to parse %r' % (c, args)) 511 return c.parse(args) 512 return do_ 513 514 method = generateDo(subCommand) 515 setattr(cmdClass, methodName, method) 516 517 518 # add help command 519 methodName = 'help_' + name 520 521 def generateHelp(c): 522 523 def help_(s): 524 command.parser.print_help(file=s.stdout) 525 return help_ 526 527 method = generateHelp(subCommand) 528 setattr(cmdClass, methodName, method) 529 530 return cmdClass 531 532
533 -def commandToCmd(command):
534 # for compatibility reasons 535 return commandToCmdClass(command)()
536