1
# -*- test-case-name: twisted.test.test_zshcomp -*-
2
# Copyright (c) 2006 Twisted Matrix Laboratories.
3
# See LICENSE for details.
6
Rebuild the completion functions for the currently active version of Twisted::
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.
13
Maintainer: Eric Mangold
15
To build completion functions for your own commands, and not Twisted commands,
16
then just do something like this::
18
o = mymodule.MyOptions()
19
f = file('_mycommand', 'w')
20
Builder("mycommand", o, f).write()
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
28
SPECIAL CLASS VARIABLES. You may set these on your usage.Options subclass::
37
Here is what they mean (with examples)::
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
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
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.
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.
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
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.
71
"colors" will allow multiple arguments to be completed, seperated by
72
commas. The possible arguments are red, green, and blue. Examples::
74
my_command --foo some-file.foo --colors=red,green
75
my_command --colors=green
76
my_command --colors=green,blue
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.
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}
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.
91
Normally Zsh does not show these descriptions unless you have
92
"verbose" completion turned on. Turn on verbosity with this in your
95
zstyle ':completion:*' verbose yes
96
zstyle ':completion:*:descriptions' format '%B%d%b'
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.
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.
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.
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}
123
To enable advanced completion add something like this to your ~/.zshrc::
128
For some extra verbosity, and general niceness add these lines too::
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'
137
import itertools, sys, commands, os.path
139
from twisted.python import reflect, util, usage
140
from twisted.scripts.mktap import IServiceMaker
142
class MyOptions(usage.Options):
144
Options for this file
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 ' \
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']
164
def __init__(self, cmd_name, options, file):
166
@type cmd_name: C{str}
167
@param cmd_name: The name of the command
169
@type options: C{twisted.usage.Options}
170
@param options: The C{twisted.usage.Options} instance defined for
174
@param file: The C{file} to write the completion function to
177
self.cmd_name = cmd_name
178
self.options = options
183
Write the completion function to the file given to __init__
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)
191
class SubcommandBuilder(Builder):
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
202
Write the completion function to the file given to __init__
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)
211
self.file.write('"%s:%s"\n' % (p.tapname, p.description))
212
self.file.write(")\n\n")
214
self.options.__class__.zsh_extras = ['*::subcmd:->subcmd']
215
gen = ArgumentsGenerator(self.cmd_name, self.options, self.file)
218
self.file.write("""if (( CURRENT == 1 )); then
219
_describe "%s" _zsh_subcmds_array && ret=0
221
(( ret )) || return 0
225
case $service in\n""" % (self.subcmdLabel,))
227
plugins = newplugin.getPlugins(self.interface)
229
self.file.write(p.tapname + ")\n")
230
gen = ArgumentsGenerator(p.tapname, p.options(), self.file)
232
self.file.write(";;\n")
233
self.file.write("*) _message \"don't know how to" \
234
" complete $service\";;\nesac")
236
class MktapBuilder(SubcommandBuilder):
238
Builder for the mktap command
240
interface = IServiceMaker
241
subcmdLabel = 'tap to build'
243
class TwistdBuilder(SubcommandBuilder):
245
Builder for the twistd command
247
interface = IServiceMaker
248
subcmdLabel = 'service to run'
250
class ArgumentsGenerator:
252
Generate a call to the zsh _arguments completion function
253
based on data in a usage.Options subclass
255
def __init__(self, cmd_name, options, file):
257
@type cmd_name: C{str}
258
@param cmd_name: The name of the command
260
@type options: C{twisted.usage.Options}
261
@param options: The C{twisted.usage.Options} instance defined
265
@param file: The C{file} to write the completion function to
267
self.cmd_name = cmd_name
268
self.options = options
271
self.altArgDescr = {}
272
self.actionDescr = {}
274
self.mutuallyExclusive = []
278
aCL = reflect.accumulateClassList
279
aCD = reflect.accumulateClassDict
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)
292
aCL(options.__class__, 'optFlags', optFlags)
293
aCL(options.__class__, 'optParameters', optParams)
295
for i, optList in enumerate(optFlags):
296
if len(optList) != 3:
297
optFlags[i] = util.padTo(3, optList)
299
for i, optList in enumerate(optParams):
300
if len(optList) != 4:
301
optParams[i] = util.padTo(4, optList)
304
self.optFlags = optFlags
305
self.optParams = optParams
308
for optList in optParams:
309
optParams_d[optList[0]] = optList[1:]
310
self.optParams_d = optParams_d
313
for optList in optFlags:
314
optFlags_d[optList[0]] = optList[1:]
315
self.optFlags_d = optFlags_d
318
optAll_d.update(optParams_d)
319
optAll_d.update(optFlags_d)
320
self.optAll_d = optAll_d
322
self.addAdditionalOptions()
324
# makes sure none of the zsh_ data structures reference option
325
# names that don't exist. (great for catching typos)
326
self.verifyZshNames()
328
self.excludes = self.makeExcludesDict()
332
Write the zsh completion code to the file given to __init__
340
def writeHeader(self):
342
This is the start of the code that calls _arguments
345
self.file.write('_arguments -s -A "-*" \\\n')
347
def writeOptions(self):
349
Write out zsh code for each option in this command
352
optNames = self.optAll_d.keys()
354
for long in optNames:
357
def writeExtras(self):
359
Write out the "extras" list. These are just passed verbatim to the
363
for s in self.extras:
364
self.file.write(escape(s))
365
self.file.write(' \\\n')
367
def writeFooter(self):
369
Write the last bit of code that finishes the call to _arguments
372
self.file.write('&& return 0\n')
374
def verifyZshNames(self):
376
Ensure that none of the names given in zsh_* variables are typoed
378
@raise ValueError: Raised if unknown option names have been given in
382
raise ValueError, "Unknown option name \"%s\" found while\n" \
383
"examining zsh_ attributes for the %s command" % (
386
for name in itertools.chain(self.altArgDescr, self.actionDescr,
387
self.actions, self.multiUse):
388
if name not in self.optAll_d:
391
for seq in self.mutuallyExclusive:
393
if name not in self.optAll_d:
396
def excludeStr(self, long, buildShort=False):
398
Generate an "exclusion string" for the given option
401
@param long: The long name of the option
402
(i.e. "verbose" instead of "v")
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
409
@return: The generated C{str}
411
if long in self.excludes:
412
exclusions = self.excludes[long][:]
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)
425
exclusions.append(long)
431
for optName in exclusions:
432
if len(optName) == 1:
434
strings.append("-" + optName)
436
strings.append("--" + optName)
437
return "(%s)" % " ".join(strings)
439
def makeExcludesDict(self):
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)
446
#create a mapping of long option name -> single character name
448
for optList in itertools.chain(self.optParams, self.optFlags):
450
if optList[1] != None:
451
longToShort[optList[0]] = optList[1]
456
for lst in self.mutuallyExclusive:
457
for i, long in enumerate(lst):
460
tmp.extend(lst[i+1:])
462
if name in longToShort:
463
tmp.append(longToShort[name])
466
excludes[long].extend(tmp)
471
def writeOpt(self, long):
473
Write out the zsh code for the given argument. This is just part of the
474
one big call to _arguments
477
@param long: The long name of the option
478
(i.e. "verbose" instead of "v")
482
if long in self.optFlags_d:
483
# It's a flag option. Not one that takes a parameter.
484
long_field = "--%s" % long
486
long_field = "--%s=" % long
488
short = self.getShortOption(long)
490
short_field = "-" + short
494
descr = self.getDescription(long)
495
descr_field = descr.replace("[", "\[")
496
descr_field = descr_field.replace("]", "\]")
497
descr_field = '[%s]' % descr_field
499
if long in self.actionDescr:
500
actionDescr_field = self.actionDescr[long]
502
actionDescr_field = descr
504
action_field = self.getAction(long)
505
if long in self.multiUse:
510
longExclusions_field = self.excludeStr(long)
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')
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')
523
def getAction(self, long):
525
Return a zsh "action" string for the given argument
528
if long in self.actions:
529
if callable(self.actions[long]):
530
action = self.actions[long]()
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)
538
def getActionDescr(self, long):
540
Return the description to be used when this argument is completed
543
if long in self.actionDescr:
544
return self.actionDescr[long]
548
def getDescription(self, long):
550
Return the description to be used for this argument
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]
557
#otherwise we have to get it from the optFlags or optParams
559
descr = self.optFlags_d[long][1]
562
descr = self.optParams_d[long][2]
566
if descr is not None:
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)
573
descr = descrFromDoc(obj)
574
if descr is not None:
577
return long # we really ought to have a good description to use
579
def getShortOption(self, long):
581
Return the short option letter or None
582
@return: C{str} or C{None}
584
optList = self.optAll_d[long]
586
return optList[0] or None
590
def addAdditionalOptions(self):
592
Add additional options to the optFlags and optParams lists.
593
These will be defined by 'opt_foo' methods of the Options subclass
597
reflect.accumulateMethods(self.options, methodsDict, 'opt_')
599
for name in methodsDict.copy():
601
methodToShort[methodsDict[name]] = name
602
del methodsDict[name]
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:
611
descr = self.getDescription(long)
614
if methodObj in methodToShort:
615
short = methodToShort[methodObj]
617
reqArgs = methodObj.im_func.func_code.co_argcount
619
self.optParams.append([long, short, None, descr])
620
self.optParams_d[long] = [short, None, descr]
621
self.optAll_d[long] = [short, None, descr]
623
self.optFlags.append([long, short, descr])
624
self.optFlags_d[long] = [short, descr]
625
self.optAll_d[long] = [short, None, descr]
627
raise TypeError, '%r has wrong number ' \
628
'of arguments' % (methodObj,)
630
def descrFromDoc(obj):
632
Generate an appropriate description from docstring of the given object
634
if obj.__doc__ is None:
637
lines = obj.__doc__.split("\n")
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()
651
Return the first line of the given string
661
Shell escape the given string
663
return commands.mkarg(str)[1:]
665
def siteFunctionsPath():
667
Return the path to the system-wide site-functions directory or
668
C{None} if it cannot be determined
671
cmd = "zsh -f -c 'echo ${(M)fpath:#/*/site-functions}'"
672
output = commands.getoutput(cmd)
673
if os.path.isdir(output):
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),
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
701
specialBuilders = {'mktap' : MktapBuilder,
702
'twistd' : TwistdBuilder}
704
def makeCompFunctionFiles(out_path, generateFor=generateFor,
705
specialBuilders=specialBuilders):
707
Generate completion function files in the given directory for all
710
@type out_path: C{str}
711
@param out_path: The path to the directory to generate completion function
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.
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
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,
731
for cmd_name, module_name, class_name in generateFor:
732
if module_name is None:
734
f = _openCmdFile(out_path, cmd_name)
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
742
if cmd_name in specialBuilders:
743
b = specialBuilders[cmd_name](cmd_name, o, f)
746
b = Builder(cmd_name, o, f)
749
skips.append( (cmd_name, e) )
753
def _openCmdFile(out_path, cmd_name):
754
return file(os.path.join(out_path, '_'+cmd_name), 'w')
757
options = MyOptions()
759
options.parseOptions(sys.argv[1:])
760
except usage.UsageError, e:
762
print options.getUsage()
765
if options['install']:
767
dir = os.path.join(os.path.dirname(twisted.__file__), "python", "zsh")
768
skips = makeCompFunctionFiles(dir)
770
skips = makeCompFunctionFiles(options['directory'])
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')
779
if __name__ == '__main__':