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 |