| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.test.test_zshcomp -*- | |
| 2 # Copyright (c) 2006 Twisted Matrix Laboratories. | |
| 3 # See LICENSE for details. | |
| 4 | |
| 5 """ | |
| 6 Rebuild the completion functions for the currently active version of Twisted:: | |
| 7 $ python zshcomp.py -i | |
| 8 | |
| 9 This module implements a zsh code generator which generates completion code for | |
| 10 commands that use twisted.python.usage. This is the stuff that makes pressing | |
| 11 Tab at the command line work. | |
| 12 | |
| 13 Maintainer: Eric Mangold | |
| 14 | |
| 15 To build completion functions for your own commands, and not Twisted commands, | |
| 16 then just do something like this:: | |
| 17 | |
| 18 o = mymodule.MyOptions() | |
| 19 f = file('_mycommand', 'w') | |
| 20 Builder("mycommand", o, f).write() | |
| 21 | |
| 22 Then all you have to do is place the generated file somewhere in your | |
| 23 C{$fpath}, and restart zsh. Note the "site-functions" directory in your | |
| 24 C{$fpath} where you may install 3rd-party completion functions (like the one | |
| 25 you're building). Call C{siteFunctionsPath} to locate this directory | |
| 26 programmatically. | |
| 27 | |
| 28 SPECIAL CLASS VARIABLES. You may set these on your usage.Options subclass:: | |
| 29 | |
| 30 zsh_altArgDescr | |
| 31 zsh_multiUse | |
| 32 zsh_mutuallyExclusive | |
| 33 zsh_actions | |
| 34 zsh_actionDescr | |
| 35 zsh_extras | |
| 36 | |
| 37 Here is what they mean (with examples):: | |
| 38 | |
| 39 zsh_altArgDescr = {"foo":"use this description for foo instead"} | |
| 40 A dict mapping long option names to alternate descriptions. When this | |
| 41 variable is present, the descriptions contained here will override | |
| 42 those descriptions provided in the optFlags and optParameters | |
| 43 variables. | |
| 44 | |
| 45 zsh_multiUse = ["foo", "bar"] | |
| 46 A sequence containing those long option names which may appear on the | |
| 47 command line more than once. By default, options will only be completed | |
| 48 one time. | |
| 49 | |
| 50 zsh_mutuallyExclusive = [("foo", "bar"), ("bar", "baz")] | |
| 51 A sequence of sequences, with each sub-sequence containing those long | |
| 52 option names that are mutually exclusive. That is, those options that | |
| 53 cannot appear on the command line together. | |
| 54 | |
| 55 zsh_actions = {"foo":'_files -g "*.foo"', "bar":"(one two three)", | |
| 56 "colors":"_values -s , 'colors to use' red green blue"} | |
| 57 A dict mapping long option names to Zsh "actions". These actions | |
| 58 define what will be completed as the argument to the given option. By | |
| 59 default, all files/dirs will be completed if no action is given. | |
| 60 | |
| 61 Callables may instead be given for the values in this dict. The | |
| 62 callable should accept no arguments, and return a string that will be | |
| 63 used as the zsh "action" in the same way as the literal strings in the | |
| 64 examples above. | |
| 65 | |
| 66 As you can see in the example above. The "foo" option will have files | |
| 67 that end in .foo completed when the user presses Tab. The "bar" | |
| 68 option will have either of the strings "one", "two", or "three" | |
| 69 completed when the user presses Tab. | |
| 70 | |
| 71 "colors" will allow multiple arguments to be completed, seperated by | |
| 72 commas. The possible arguments are red, green, and blue. Examples:: | |
| 73 | |
| 74 my_command --foo some-file.foo --colors=red,green | |
| 75 my_command --colors=green | |
| 76 my_command --colors=green,blue | |
| 77 | |
| 78 Actions may take many forms, and it is beyond the scope of this | |
| 79 document to illustrate them all. Please refer to the documention for | |
| 80 the Zsh _arguments function. zshcomp is basically a front-end to Zsh's | |
| 81 _arguments completion function. | |
| 82 | |
| 83 That documentation is available on the zsh web site at this URL: | |
| 84 U{http://zsh.sunsite.dk/Doc/Release/zsh_19.html#SEC124} | |
| 85 | |
| 86 zsh_actionDescr = {"logfile":"log file name", "random":"random seed"} | |
| 87 A dict mapping long option names to a description for the corresponding | |
| 88 zsh "action". These descriptions are show above the generated matches | |
| 89 when the user is doing completions for this option. | |
| 90 | |
| 91 Normally Zsh does not show these descriptions unless you have | |
| 92 "verbose" completion turned on. Turn on verbosity with this in your | |
| 93 ~/.zshrc:: | |
| 94 | |
| 95 zstyle ':completion:*' verbose yes | |
| 96 zstyle ':completion:*:descriptions' format '%B%d%b' | |
| 97 | |
| 98 zsh_extras = [":file to read from:action", ":file to write to:action"] | |
| 99 A sequence of extra arguments that will be passed verbatim to Zsh's | |
| 100 _arguments completion function. The _arguments function does all the | |
| 101 hard work of doing command line completions. You can see how zshcomp | |
| 102 invokes the _arguments call by looking at the generated completion | |
| 103 files that this module creates. | |
| 104 | |
| 105 *** NOTE *** | |
| 106 | |
| 107 You will need to use this variable to describe completions for normal | |
| 108 command line arguments. That is, those arguments that are not | |
| 109 associated with an option. That is, the arguments that are given to the | |
| 110 parseArgs method of your usage.Options subclass. | |
| 111 | |
| 112 In the example above, the 1st non-option argument will be described as | |
| 113 "file to read from" and completion options will be generated in | |
| 114 accordance with the "action". (See above about zsh "actions") The | |
| 115 2nd non-option argument will be described as "file to write to" and | |
| 116 the action will be interpreted likewise. | |
| 117 | |
| 118 Things you can put here are all documented under the _arguments | |
| 119 function here: U{http://zsh.sunsite.dk/Doc/Release/zsh_19.html#SEC124} | |
| 120 | |
| 121 Zsh Notes: | |
| 122 | |
| 123 To enable advanced completion add something like this to your ~/.zshrc:: | |
| 124 | |
| 125 autoload -U compinit | |
| 126 compinit | |
| 127 | |
| 128 For some extra verbosity, and general niceness add these lines too:: | |
| 129 | |
| 130 zstyle ':completion:*' verbose yes | |
| 131 zstyle ':completion:*:descriptions' format '%B%d%b' | |
| 132 zstyle ':completion:*:messages' format '%d' | |
| 133 zstyle ':completion:*:warnings' format 'No matches for: %d' | |
| 134 | |
| 135 Have fun! | |
| 136 """ | |
| 137 import itertools, sys, commands, os.path | |
| 138 | |
| 139 from twisted.python import reflect, util, usage | |
| 140 from twisted.scripts.mktap import IServiceMaker | |
| 141 | |
| 142 class MyOptions(usage.Options): | |
| 143 """ | |
| 144 Options for this file | |
| 145 """ | |
| 146 longdesc = "" | |
| 147 synopsis = "Usage: python zshcomp.py [--install | -i] | <output directory>" | |
| 148 optFlags = [["install", "i", | |
| 149 'Output files to the "installation" directory ' \ | |
| 150 '(twisted/python/zsh in the currently active ' \ | |
| 151 'Twisted package)']] | |
| 152 optParameters = [["directory", "d", None, | |
| 153 "Output files to this directory"]] | |
| 154 def postOptions(self): | |
| 155 if self['install'] and self['directory']: | |
| 156 raise usage.UsageError, "Can't have --install and " \ | |
| 157 "--directory at the same time" | |
| 158 if not self['install'] and not self['directory']: | |
| 159 raise usage.UsageError, "Not enough arguments" | |
| 160 if self['directory'] and not os.path.isdir(self['directory']): | |
| 161 raise usage.UsageError, "%s is not a directory" % self['directory'] | |
| 162 | |
| 163 class Builder: | |
| 164 def __init__(self, cmd_name, options, file): | |
| 165 """ | |
| 166 @type cmd_name: C{str} | |
| 167 @param cmd_name: The name of the command | |
| 168 | |
| 169 @type options: C{twisted.usage.Options} | |
| 170 @param options: The C{twisted.usage.Options} instance defined for | |
| 171 this command | |
| 172 | |
| 173 @type file: C{file} | |
| 174 @param file: The C{file} to write the completion function to | |
| 175 """ | |
| 176 | |
| 177 self.cmd_name = cmd_name | |
| 178 self.options = options | |
| 179 self.file = file | |
| 180 | |
| 181 def write(self): | |
| 182 """ | |
| 183 Write the completion function to the file given to __init__ | |
| 184 @return: C{None} | |
| 185 """ | |
| 186 # by default, we just write out a single call to _arguments | |
| 187 self.file.write('#compdef %s\n' % (self.cmd_name,)) | |
| 188 gen = ArgumentsGenerator(self.cmd_name, self.options, self.file) | |
| 189 gen.write() | |
| 190 | |
| 191 class SubcommandBuilder(Builder): | |
| 192 """ | |
| 193 Use this builder for commands that have sub-commands. twisted.python.usage | |
| 194 has the notion of sub-commands that are defined using an entirely seperate | |
| 195 Options class. | |
| 196 """ | |
| 197 interface = None | |
| 198 subcmdLabel = None | |
| 199 | |
| 200 def write(self): | |
| 201 """ | |
| 202 Write the completion function to the file given to __init__ | |
| 203 @return: C{None} | |
| 204 """ | |
| 205 self.file.write('#compdef %s\n' % (self.cmd_name,)) | |
| 206 self.file.write('local _zsh_subcmds_array\n_zsh_subcmds_array=(\n') | |
| 207 from twisted import plugin as newplugin | |
| 208 plugins = newplugin.getPlugins(self.interface) | |
| 209 | |
| 210 for p in plugins: | |
| 211 self.file.write('"%s:%s"\n' % (p.tapname, p.description)) | |
| 212 self.file.write(")\n\n") | |
| 213 | |
| 214 self.options.__class__.zsh_extras = ['*::subcmd:->subcmd'] | |
| 215 gen = ArgumentsGenerator(self.cmd_name, self.options, self.file) | |
| 216 gen.write() | |
| 217 | |
| 218 self.file.write("""if (( CURRENT == 1 )); then | |
| 219 _describe "%s" _zsh_subcmds_array && ret=0 | |
| 220 fi | |
| 221 (( ret )) || return 0 | |
| 222 | |
| 223 service="$words[1]" | |
| 224 | |
| 225 case $service in\n""" % (self.subcmdLabel,)) | |
| 226 | |
| 227 plugins = newplugin.getPlugins(self.interface) | |
| 228 for p in plugins: | |
| 229 self.file.write(p.tapname + ")\n") | |
| 230 gen = ArgumentsGenerator(p.tapname, p.options(), self.file) | |
| 231 gen.write() | |
| 232 self.file.write(";;\n") | |
| 233 self.file.write("*) _message \"don't know how to" \ | |
| 234 " complete $service\";;\nesac") | |
| 235 | |
| 236 class MktapBuilder(SubcommandBuilder): | |
| 237 """ | |
| 238 Builder for the mktap command | |
| 239 """ | |
| 240 interface = IServiceMaker | |
| 241 subcmdLabel = 'tap to build' | |
| 242 | |
| 243 class TwistdBuilder(SubcommandBuilder): | |
| 244 """ | |
| 245 Builder for the twistd command | |
| 246 """ | |
| 247 interface = IServiceMaker | |
| 248 subcmdLabel = 'service to run' | |
| 249 | |
| 250 class ArgumentsGenerator: | |
| 251 """ | |
| 252 Generate a call to the zsh _arguments completion function | |
| 253 based on data in a usage.Options subclass | |
| 254 """ | |
| 255 def __init__(self, cmd_name, options, file): | |
| 256 """ | |
| 257 @type cmd_name: C{str} | |
| 258 @param cmd_name: The name of the command | |
| 259 | |
| 260 @type options: C{twisted.usage.Options} | |
| 261 @param options: The C{twisted.usage.Options} instance defined | |
| 262 for this command | |
| 263 | |
| 264 @type file: C{file} | |
| 265 @param file: The C{file} to write the completion function to | |
| 266 """ | |
| 267 self.cmd_name = cmd_name | |
| 268 self.options = options | |
| 269 self.file = file | |
| 270 | |
| 271 self.altArgDescr = {} | |
| 272 self.actionDescr = {} | |
| 273 self.multiUse = [] | |
| 274 self.mutuallyExclusive = [] | |
| 275 self.actions = {} | |
| 276 self.extras = [] | |
| 277 | |
| 278 aCL = reflect.accumulateClassList | |
| 279 aCD = reflect.accumulateClassDict | |
| 280 | |
| 281 aCD(options.__class__, 'zsh_altArgDescr', self.altArgDescr) | |
| 282 aCD(options.__class__, 'zsh_actionDescr', self.actionDescr) | |
| 283 aCL(options.__class__, 'zsh_multiUse', self.multiUse) | |
| 284 aCL(options.__class__, 'zsh_mutuallyExclusive', | |
| 285 self.mutuallyExclusive) | |
| 286 aCD(options.__class__, 'zsh_actions', self.actions) | |
| 287 aCL(options.__class__, 'zsh_extras', self.extras) | |
| 288 | |
| 289 optFlags = [] | |
| 290 optParams = [] | |
| 291 | |
| 292 aCL(options.__class__, 'optFlags', optFlags) | |
| 293 aCL(options.__class__, 'optParameters', optParams) | |
| 294 | |
| 295 for i, optList in enumerate(optFlags): | |
| 296 if len(optList) != 3: | |
| 297 optFlags[i] = util.padTo(3, optList) | |
| 298 | |
| 299 for i, optList in enumerate(optParams): | |
| 300 if len(optList) != 4: | |
| 301 optParams[i] = util.padTo(4, optList) | |
| 302 | |
| 303 | |
| 304 self.optFlags = optFlags | |
| 305 self.optParams = optParams | |
| 306 | |
| 307 optParams_d = {} | |
| 308 for optList in optParams: | |
| 309 optParams_d[optList[0]] = optList[1:] | |
| 310 self.optParams_d = optParams_d | |
| 311 | |
| 312 optFlags_d = {} | |
| 313 for optList in optFlags: | |
| 314 optFlags_d[optList[0]] = optList[1:] | |
| 315 self.optFlags_d = optFlags_d | |
| 316 | |
| 317 optAll_d = {} | |
| 318 optAll_d.update(optParams_d) | |
| 319 optAll_d.update(optFlags_d) | |
| 320 self.optAll_d = optAll_d | |
| 321 | |
| 322 self.addAdditionalOptions() | |
| 323 | |
| 324 # makes sure none of the zsh_ data structures reference option | |
| 325 # names that don't exist. (great for catching typos) | |
| 326 self.verifyZshNames() | |
| 327 | |
| 328 self.excludes = self.makeExcludesDict() | |
| 329 | |
| 330 def write(self): | |
| 331 """ | |
| 332 Write the zsh completion code to the file given to __init__ | |
| 333 @return: C{None} | |
| 334 """ | |
| 335 self.writeHeader() | |
| 336 self.writeExtras() | |
| 337 self.writeOptions() | |
| 338 self.writeFooter() | |
| 339 | |
| 340 def writeHeader(self): | |
| 341 """ | |
| 342 This is the start of the code that calls _arguments | |
| 343 @return: C{None} | |
| 344 """ | |
| 345 self.file.write('_arguments -s -A "-*" \\\n') | |
| 346 | |
| 347 def writeOptions(self): | |
| 348 """ | |
| 349 Write out zsh code for each option in this command | |
| 350 @return: C{None} | |
| 351 """ | |
| 352 optNames = self.optAll_d.keys() | |
| 353 optNames.sort() | |
| 354 for long in optNames: | |
| 355 self.writeOpt(long) | |
| 356 | |
| 357 def writeExtras(self): | |
| 358 """ | |
| 359 Write out the "extras" list. These are just passed verbatim to the | |
| 360 _arguments call | |
| 361 @return: C{None} | |
| 362 """ | |
| 363 for s in self.extras: | |
| 364 self.file.write(escape(s)) | |
| 365 self.file.write(' \\\n') | |
| 366 | |
| 367 def writeFooter(self): | |
| 368 """ | |
| 369 Write the last bit of code that finishes the call to _arguments | |
| 370 @return: C{None} | |
| 371 """ | |
| 372 self.file.write('&& return 0\n') | |
| 373 | |
| 374 def verifyZshNames(self): | |
| 375 """ | |
| 376 Ensure that none of the names given in zsh_* variables are typoed | |
| 377 @return: C{None} | |
| 378 @raise ValueError: Raised if unknown option names have been given in | |
| 379 zsh_* variables | |
| 380 """ | |
| 381 def err(name): | |
| 382 raise ValueError, "Unknown option name \"%s\" found while\n" \ | |
| 383 "examining zsh_ attributes for the %s command" % ( | |
| 384 name, self.cmd_name) | |
| 385 | |
| 386 for name in itertools.chain(self.altArgDescr, self.actionDescr, | |
| 387 self.actions, self.multiUse): | |
| 388 if name not in self.optAll_d: | |
| 389 err(name) | |
| 390 | |
| 391 for seq in self.mutuallyExclusive: | |
| 392 for name in seq: | |
| 393 if name not in self.optAll_d: | |
| 394 err(name) | |
| 395 | |
| 396 def excludeStr(self, long, buildShort=False): | |
| 397 """ | |
| 398 Generate an "exclusion string" for the given option | |
| 399 | |
| 400 @type long: C{str} | |
| 401 @param long: The long name of the option | |
| 402 (i.e. "verbose" instead of "v") | |
| 403 | |
| 404 @type buildShort: C{bool} | |
| 405 @param buildShort: May be True to indicate we're building an excludes | |
| 406 string for the short option that correspondes to | |
| 407 the given long opt | |
| 408 | |
| 409 @return: The generated C{str} | |
| 410 """ | |
| 411 if long in self.excludes: | |
| 412 exclusions = self.excludes[long][:] | |
| 413 else: | |
| 414 exclusions = [] | |
| 415 | |
| 416 # if long isn't a multiUse option (can't appear on the cmd line more | |
| 417 # than once), then we have to exclude the short option if we're | |
| 418 # building for the long option, and vice versa. | |
| 419 if long not in self.multiUse: | |
| 420 if buildShort is False: | |
| 421 short = self.getShortOption(long) | |
| 422 if short is not None: | |
| 423 exclusions.append(short) | |
| 424 else: | |
| 425 exclusions.append(long) | |
| 426 | |
| 427 if not exclusions: | |
| 428 return '' | |
| 429 | |
| 430 strings = [] | |
| 431 for optName in exclusions: | |
| 432 if len(optName) == 1: | |
| 433 # short option | |
| 434 strings.append("-" + optName) | |
| 435 else: | |
| 436 strings.append("--" + optName) | |
| 437 return "(%s)" % " ".join(strings) | |
| 438 | |
| 439 def makeExcludesDict(self): | |
| 440 """ | |
| 441 @return: A C{dict} that maps each option name appearing in | |
| 442 self.mutuallyExclusive to a list of those option names that | |
| 443 is it mutually exclusive with (can't appear on the cmd line with) | |
| 444 """ | |
| 445 | |
| 446 #create a mapping of long option name -> single character name | |
| 447 longToShort = {} | |
| 448 for optList in itertools.chain(self.optParams, self.optFlags): | |
| 449 try: | |
| 450 if optList[1] != None: | |
| 451 longToShort[optList[0]] = optList[1] | |
| 452 except IndexError: | |
| 453 pass | |
| 454 | |
| 455 excludes = {} | |
| 456 for lst in self.mutuallyExclusive: | |
| 457 for i, long in enumerate(lst): | |
| 458 tmp = [] | |
| 459 tmp.extend(lst[:i]) | |
| 460 tmp.extend(lst[i+1:]) | |
| 461 for name in tmp[:]: | |
| 462 if name in longToShort: | |
| 463 tmp.append(longToShort[name]) | |
| 464 | |
| 465 if long in excludes: | |
| 466 excludes[long].extend(tmp) | |
| 467 else: | |
| 468 excludes[long] = tmp | |
| 469 return excludes | |
| 470 | |
| 471 def writeOpt(self, long): | |
| 472 """ | |
| 473 Write out the zsh code for the given argument. This is just part of the | |
| 474 one big call to _arguments | |
| 475 | |
| 476 @type long: C{str} | |
| 477 @param long: The long name of the option | |
| 478 (i.e. "verbose" instead of "v") | |
| 479 | |
| 480 @return: C{None} | |
| 481 """ | |
| 482 if long in self.optFlags_d: | |
| 483 # It's a flag option. Not one that takes a parameter. | |
| 484 long_field = "--%s" % long | |
| 485 else: | |
| 486 long_field = "--%s=" % long | |
| 487 | |
| 488 short = self.getShortOption(long) | |
| 489 if short != None: | |
| 490 short_field = "-" + short | |
| 491 else: | |
| 492 short_field = '' | |
| 493 | |
| 494 descr = self.getDescription(long) | |
| 495 descr_field = descr.replace("[", "\[") | |
| 496 descr_field = descr_field.replace("]", "\]") | |
| 497 descr_field = '[%s]' % descr_field | |
| 498 | |
| 499 if long in self.actionDescr: | |
| 500 actionDescr_field = self.actionDescr[long] | |
| 501 else: | |
| 502 actionDescr_field = descr | |
| 503 | |
| 504 action_field = self.getAction(long) | |
| 505 if long in self.multiUse: | |
| 506 multi_field = '*' | |
| 507 else: | |
| 508 multi_field = '' | |
| 509 | |
| 510 longExclusions_field = self.excludeStr(long) | |
| 511 | |
| 512 if short: | |
| 513 #we have to write an extra line for the short option if we have one | |
| 514 shortExclusions_field = self.excludeStr(long, buildShort=True) | |
| 515 self.file.write(escape('%s%s%s%s%s' % (shortExclusions_field, | |
| 516 multi_field, short_field, descr_field, action_field))) | |
| 517 self.file.write(' \\\n') | |
| 518 | |
| 519 self.file.write(escape('%s%s%s%s%s' % (longExclusions_field, | |
| 520 multi_field, long_field, descr_field, action_field))) | |
| 521 self.file.write(' \\\n') | |
| 522 | |
| 523 def getAction(self, long): | |
| 524 """ | |
| 525 Return a zsh "action" string for the given argument | |
| 526 @return: C{str} | |
| 527 """ | |
| 528 if long in self.actions: | |
| 529 if callable(self.actions[long]): | |
| 530 action = self.actions[long]() | |
| 531 else: | |
| 532 action = self.actions[long] | |
| 533 return ":%s:%s" % (self.getActionDescr(long), action) | |
| 534 if long in self.optParams_d: | |
| 535 return ':%s:_files' % self.getActionDescr(long) | |
| 536 return '' | |
| 537 | |
| 538 def getActionDescr(self, long): | |
| 539 """ | |
| 540 Return the description to be used when this argument is completed | |
| 541 @return: C{str} | |
| 542 """ | |
| 543 if long in self.actionDescr: | |
| 544 return self.actionDescr[long] | |
| 545 else: | |
| 546 return long | |
| 547 | |
| 548 def getDescription(self, long): | |
| 549 """ | |
| 550 Return the description to be used for this argument | |
| 551 @return: C{str} | |
| 552 """ | |
| 553 #check if we have an alternate descr for this arg, and if so use it | |
| 554 if long in self.altArgDescr: | |
| 555 return self.altArgDescr[long] | |
| 556 | |
| 557 #otherwise we have to get it from the optFlags or optParams | |
| 558 try: | |
| 559 descr = self.optFlags_d[long][1] | |
| 560 except KeyError: | |
| 561 try: | |
| 562 descr = self.optParams_d[long][2] | |
| 563 except KeyError: | |
| 564 descr = None | |
| 565 | |
| 566 if descr is not None: | |
| 567 return descr | |
| 568 | |
| 569 # lets try to get it from the opt_foo method doc string if there is one | |
| 570 longMangled = long.replace('-', '_') # this is what t.p.usage does | |
| 571 obj = getattr(self.options, 'opt_%s' % longMangled, None) | |
| 572 if obj: | |
| 573 descr = descrFromDoc(obj) | |
| 574 if descr is not None: | |
| 575 return descr | |
| 576 | |
| 577 return long # we really ought to have a good description to use | |
| 578 | |
| 579 def getShortOption(self, long): | |
| 580 """ | |
| 581 Return the short option letter or None | |
| 582 @return: C{str} or C{None} | |
| 583 """ | |
| 584 optList = self.optAll_d[long] | |
| 585 try: | |
| 586 return optList[0] or None | |
| 587 except IndexError: | |
| 588 pass | |
| 589 | |
| 590 def addAdditionalOptions(self): | |
| 591 """ | |
| 592 Add additional options to the optFlags and optParams lists. | |
| 593 These will be defined by 'opt_foo' methods of the Options subclass | |
| 594 @return: C{None} | |
| 595 """ | |
| 596 methodsDict = {} | |
| 597 reflect.accumulateMethods(self.options, methodsDict, 'opt_') | |
| 598 methodToShort = {} | |
| 599 for name in methodsDict.copy(): | |
| 600 if len(name) == 1: | |
| 601 methodToShort[methodsDict[name]] = name | |
| 602 del methodsDict[name] | |
| 603 | |
| 604 for methodName, methodObj in methodsDict.items(): | |
| 605 long = methodName.replace('_', '-') # t.p.usage does this | |
| 606 # if this option is already defined by the optFlags or | |
| 607 # optParameters then we don't want to override that data | |
| 608 if long in self.optAll_d: | |
| 609 continue | |
| 610 | |
| 611 descr = self.getDescription(long) | |
| 612 | |
| 613 short = None | |
| 614 if methodObj in methodToShort: | |
| 615 short = methodToShort[methodObj] | |
| 616 | |
| 617 reqArgs = methodObj.im_func.func_code.co_argcount | |
| 618 if reqArgs == 2: | |
| 619 self.optParams.append([long, short, None, descr]) | |
| 620 self.optParams_d[long] = [short, None, descr] | |
| 621 self.optAll_d[long] = [short, None, descr] | |
| 622 elif reqArgs == 1: | |
| 623 self.optFlags.append([long, short, descr]) | |
| 624 self.optFlags_d[long] = [short, descr] | |
| 625 self.optAll_d[long] = [short, None, descr] | |
| 626 else: | |
| 627 raise TypeError, '%r has wrong number ' \ | |
| 628 'of arguments' % (methodObj,) | |
| 629 | |
| 630 def descrFromDoc(obj): | |
| 631 """ | |
| 632 Generate an appropriate description from docstring of the given object | |
| 633 """ | |
| 634 if obj.__doc__ is None: | |
| 635 return None | |
| 636 | |
| 637 lines = obj.__doc__.split("\n") | |
| 638 descr = None | |
| 639 try: | |
| 640 if lines[0] != "" and not lines[0].isspace(): | |
| 641 descr = lines[0].lstrip() | |
| 642 # skip first line if it's blank | |
| 643 elif lines[1] != "" and not lines[1].isspace(): | |
| 644 descr = lines[1].lstrip() | |
| 645 except IndexError: | |
| 646 pass | |
| 647 return descr | |
| 648 | |
| 649 def firstLine(s): | |
| 650 """ | |
| 651 Return the first line of the given string | |
| 652 """ | |
| 653 try: | |
| 654 i = s.index('\n') | |
| 655 return s[:i] | |
| 656 except ValueError: | |
| 657 return s | |
| 658 | |
| 659 def escape(str): | |
| 660 """ | |
| 661 Shell escape the given string | |
| 662 """ | |
| 663 return commands.mkarg(str)[1:] | |
| 664 | |
| 665 def siteFunctionsPath(): | |
| 666 """ | |
| 667 Return the path to the system-wide site-functions directory or | |
| 668 C{None} if it cannot be determined | |
| 669 """ | |
| 670 try: | |
| 671 cmd = "zsh -f -c 'echo ${(M)fpath:#/*/site-functions}'" | |
| 672 output = commands.getoutput(cmd) | |
| 673 if os.path.isdir(output): | |
| 674 return output | |
| 675 except: | |
| 676 pass | |
| 677 | |
| 678 generateFor = [('conch', 'twisted.conch.scripts.conch', 'ClientOptions'), | |
| 679 ('mktap', 'twisted.scripts.mktap', 'FirstPassOptions'), | |
| 680 ('trial', 'twisted.scripts.trial', 'Options'), | |
| 681 ('cftp', 'twisted.conch.scripts.cftp', 'ClientOptions'), | |
| 682 ('tapconvert', 'twisted.scripts.tapconvert', 'ConvertOptions'), | |
| 683 ('twistd', 'twisted.scripts.twistd', 'ServerOptions'), | |
| 684 ('ckeygen', 'twisted.conch.scripts.ckeygen', 'GeneralOptions'), | |
| 685 ('lore', 'twisted.lore.scripts.lore', 'Options'), | |
| 686 ('pyhtmlizer', 'twisted.scripts.htmlizer', 'Options'), | |
| 687 ('tap2deb', 'twisted.scripts.tap2deb', 'MyOptions'), | |
| 688 ('tkconch', 'twisted.conch.scripts.tkconch', 'GeneralOptions'), | |
| 689 ('manhole', 'twisted.scripts.manhole', 'MyOptions'), | |
| 690 ('tap2rpm', 'twisted.scripts.tap2rpm', 'MyOptions'), | |
| 691 ('websetroot', None, None), | |
| 692 ('tkmktap', None, None), | |
| 693 ] | |
| 694 # NOTE: the commands using None above are no longer included in Twisted. | |
| 695 # However due to limitations in zsh's completion system the version of | |
| 696 # _twisted_zsh_stub shipped with zsh contains a static list of Twisted's | |
| 697 # commands. It will display errors if completion functions for these missing | |
| 698 # commands are not found :( So we just include dummy (empty) completion | |
| 699 # function files | |
| 700 | |
| 701 specialBuilders = {'mktap' : MktapBuilder, | |
| 702 'twistd' : TwistdBuilder} | |
| 703 | |
| 704 def makeCompFunctionFiles(out_path, generateFor=generateFor, | |
| 705 specialBuilders=specialBuilders): | |
| 706 """ | |
| 707 Generate completion function files in the given directory for all | |
| 708 twisted commands | |
| 709 | |
| 710 @type out_path: C{str} | |
| 711 @param out_path: The path to the directory to generate completion function | |
| 712 fils in | |
| 713 | |
| 714 @param generateFor: Sequence in the form of the 'generateFor' top-level | |
| 715 variable as defined in this module. Indicates what | |
| 716 commands to build completion files for. | |
| 717 | |
| 718 @param specialBuilders: Sequence in the form of the 'specialBuilders' | |
| 719 top-level variable as defined in this module. | |
| 720 Indicates what commands require a special | |
| 721 Builder class. | |
| 722 | |
| 723 @return: C{list} of 2-tuples of the form (cmd_name, error) indicating | |
| 724 commands that we skipped building completions for. cmd_name | |
| 725 is the name of the skipped command, and error is the Exception | |
| 726 that was raised when trying to import the script module. | |
| 727 Commands are usually skipped due to a missing dependency, | |
| 728 e.g. Tkinter. | |
| 729 """ | |
| 730 skips = [] | |
| 731 for cmd_name, module_name, class_name in generateFor: | |
| 732 if module_name is None: | |
| 733 # create empty file | |
| 734 f = _openCmdFile(out_path, cmd_name) | |
| 735 f.close() | |
| 736 continue | |
| 737 try: | |
| 738 m = __import__('%s' % (module_name,), None, None, (class_name)) | |
| 739 f = _openCmdFile(out_path, cmd_name) | |
| 740 o = getattr(m, class_name)() # instantiate Options class | |
| 741 | |
| 742 if cmd_name in specialBuilders: | |
| 743 b = specialBuilders[cmd_name](cmd_name, o, f) | |
| 744 b.write() | |
| 745 else: | |
| 746 b = Builder(cmd_name, o, f) | |
| 747 b.write() | |
| 748 except Exception, e: | |
| 749 skips.append( (cmd_name, e) ) | |
| 750 continue | |
| 751 return skips | |
| 752 | |
| 753 def _openCmdFile(out_path, cmd_name): | |
| 754 return file(os.path.join(out_path, '_'+cmd_name), 'w') | |
| 755 | |
| 756 def run(): | |
| 757 options = MyOptions() | |
| 758 try: | |
| 759 options.parseOptions(sys.argv[1:]) | |
| 760 except usage.UsageError, e: | |
| 761 print e | |
| 762 print options.getUsage() | |
| 763 sys.exit(2) | |
| 764 | |
| 765 if options['install']: | |
| 766 import twisted | |
| 767 dir = os.path.join(os.path.dirname(twisted.__file__), "python", "zsh") | |
| 768 skips = makeCompFunctionFiles(dir) | |
| 769 else: | |
| 770 skips = makeCompFunctionFiles(options['directory']) | |
| 771 | |
| 772 for cmd_name, error in skips: | |
| 773 sys.stderr.write("zshcomp: Skipped building for %s. Script module " \ | |
| 774 "could not be imported:\n" % (cmd_name,)) | |
| 775 sys.stderr.write(str(error)+'\n') | |
| 776 if skips: | |
| 777 sys.exit(3) | |
| 778 | |
| 779 if __name__ == '__main__': | |
| 780 run() | |
| OLD | NEW |