root / autotools / build-zsh-completion @ f7ee7f53
History | View | Annotate | Download (16.5 kB)
1 |
#!/usr/bin/python |
---|---|
2 |
# |
3 |
|
4 |
# Copyright (C) 2011 Google Inc. |
5 |
# |
6 |
# This program is free software; you can redistribute it and/or modify |
7 |
# it under the terms of the GNU General Public License as published by |
8 |
# the Free Software Foundation; either version 2 of the License, or |
9 |
# (at your option) any later version. |
10 |
# |
11 |
# This program is distributed in the hope that it will be useful, but |
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 |
# General Public License for more details. |
15 |
# |
16 |
# You should have received a copy of the GNU General Public License |
17 |
# along with this program; if not, write to the Free Software |
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
19 |
# 02110-1301, USA. |
20 |
|
21 |
|
22 |
"""Script to generate zsh completion script for Ganeti. |
23 |
|
24 |
""" |
25 |
|
26 |
# pylint: disable-msg=C0103 |
27 |
# [C0103] Invalid name build-zsh-completion |
28 |
|
29 |
import os |
30 |
import re |
31 |
from cStringIO import StringIO |
32 |
|
33 |
from ganeti import constants |
34 |
from ganeti import cli |
35 |
from ganeti import utils |
36 |
from ganeti import build |
37 |
|
38 |
# _autoconf shouldn't be imported from anywhere except constants.py, but we're |
39 |
# making an exception here because this script is only used at build time. |
40 |
from ganeti import _autoconf |
41 |
|
42 |
def WritePreamble(sw): |
43 |
"""Writes the script preamble. |
44 |
|
45 |
Helper functions should be written here. |
46 |
|
47 |
""" |
48 |
sw.Write("#compdef %s" % " ".join(_autoconf.GNT_SCRIPTS)) |
49 |
sw.Write("") |
50 |
sw.Write("# This script is automatically generated at build time.") |
51 |
sw.Write("# Do not modify manually.") |
52 |
|
53 |
sw.Write("_ganeti_dbglog() {") |
54 |
sw.IncIndent() |
55 |
try: |
56 |
sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then") |
57 |
sw.IncIndent() |
58 |
try: |
59 |
sw.Write("{") |
60 |
sw.IncIndent() |
61 |
try: |
62 |
sw.Write("echo ---") |
63 |
sw.Write("echo \"$@\"") |
64 |
sw.Write("echo") |
65 |
finally: |
66 |
sw.DecIndent() |
67 |
sw.Write("} >> $GANETI_COMPL_LOG") |
68 |
finally: |
69 |
sw.DecIndent() |
70 |
sw.Write("fi") |
71 |
finally: |
72 |
sw.DecIndent() |
73 |
sw.Write("}") |
74 |
|
75 |
sw.Write("_ganeti_nodes() {") |
76 |
sw.IncIndent() |
77 |
try: |
78 |
node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list") |
79 |
sw.Write("compadd -X 'completing nodes' ${(f)\"$(cat %s 2>/dev/null)\"}", |
80 |
utils.ShellQuote(node_list_path)) |
81 |
finally: |
82 |
sw.DecIndent() |
83 |
sw.Write("}") |
84 |
|
85 |
sw.Write("_ganeti_many_nodes() {") |
86 |
sw.IncIndent() |
87 |
try: |
88 |
node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list") |
89 |
sw.Write("if compset -P '*,'; then") |
90 |
sw.IncIndent() |
91 |
try: |
92 |
sw.Write("compadd -X 'completing nodes' -S ',' -q" |
93 |
" ${(f)\"$(grep -vxF \"$(echo \"$IPREFIX\" |" |
94 |
" sed 's/,/\\n/g')\" %s 2>/dev/null)\"}", |
95 |
utils.ShellQuote(node_list_path)) |
96 |
finally: |
97 |
sw.DecIndent() |
98 |
sw.Write("else") |
99 |
sw.IncIndent() |
100 |
try: |
101 |
sw.Write("compadd -X 'completing nodes' -S ','" |
102 |
" -q ${(f)\"$(cat %s 2>/dev/null)\"}", |
103 |
utils.ShellQuote(node_list_path)) |
104 |
finally: |
105 |
sw.DecIndent() |
106 |
sw.Write("fi") |
107 |
finally: |
108 |
sw.DecIndent() |
109 |
sw.Write("}") |
110 |
|
111 |
sw.Write("_ganeti_instance_nodes() {") |
112 |
sw.IncIndent() |
113 |
sw.Write("# Complete primary and optionally secondary instance node") |
114 |
try: |
115 |
node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list") |
116 |
sw.Write("if compset -P 1 '*:'; then") |
117 |
sw.IncIndent() |
118 |
try: |
119 |
sw.Write("compadd -X 'completing nodes'" |
120 |
" ${(f)\"$(grep -vx \"${IPREFIX%%:}\" %s 2>/dev/null)\"}", |
121 |
utils.ShellQuote(node_list_path)) |
122 |
finally: |
123 |
sw.DecIndent() |
124 |
sw.Write("else") |
125 |
sw.IncIndent() |
126 |
try: |
127 |
sw.Write("compadd -X 'completing nodes' -S ':' -q" |
128 |
" ${(f)\"$(cat %s 2>/dev/null)\"}", |
129 |
utils.ShellQuote(node_list_path)) |
130 |
finally: |
131 |
sw.DecIndent() |
132 |
finally: |
133 |
sw.DecIndent() |
134 |
sw.Write("fi") |
135 |
sw.Write("}") |
136 |
|
137 |
sw.Write("_ganeti_instances() {") |
138 |
sw.IncIndent() |
139 |
try: |
140 |
instance_list_path = os.path.join(constants.DATA_DIR, |
141 |
"ssconf_instance_list") |
142 |
sw.Write("compadd -X 'completing instances'" |
143 |
" ${(f)\"$(cat %s 2>/dev/null)\"}", |
144 |
utils.ShellQuote(instance_list_path)) |
145 |
finally: |
146 |
sw.DecIndent() |
147 |
sw.Write("}") |
148 |
|
149 |
sw.Write("_ganeti_jobs() {") |
150 |
sw.IncIndent() |
151 |
try: |
152 |
# FIXME: this is really going into the internals of the job queue |
153 |
sw.Write("_description job expl 'job id'") |
154 |
sw.Write("compadd ${(f)\"$(cd %s 2>/dev/null && echo job-* |" |
155 |
" sed 's/job-//g' | xargs -n 1)\"}", |
156 |
utils.ShellQuote(constants.QUEUE_DIR)) |
157 |
finally: |
158 |
sw.DecIndent() |
159 |
sw.Write("}") |
160 |
|
161 |
for (fnname, paths) in [ |
162 |
("os", constants.OS_SEARCH_PATH), |
163 |
("iallocator", constants.IALLOCATOR_SEARCH_PATH), |
164 |
]: |
165 |
sw.Write("_ganeti_%s() {", fnname) |
166 |
sw.IncIndent() |
167 |
try: |
168 |
# FIXME: Make querying the master for all OSes cheap |
169 |
for path in paths: |
170 |
sw.Write("compadd -X 'completing %s'" |
171 |
" ${(f)\"$( cd %s 2>/dev/null && echo * | xargs -n 1)\"}" % |
172 |
(fnname, |
173 |
utils.ShellQuote(path))) |
174 |
finally: |
175 |
sw.DecIndent() |
176 |
sw.Write("}") |
177 |
|
178 |
sw.Write("_ganeti_nodegroup() {") |
179 |
sw.IncIndent() |
180 |
try: |
181 |
nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups") |
182 |
sw.Write("compadd -X 'completing node group'" |
183 |
" ${(f)\"$(cat %s | xargs -n 1 | sort 2>/dev/null)\"}", |
184 |
utils.ShellQuote(nodegroups_path)) |
185 |
finally: |
186 |
sw.DecIndent() |
187 |
sw.Write("}") |
188 |
|
189 |
sw.Write("_ganeti_network() {") |
190 |
sw.IncIndent() |
191 |
try: |
192 |
networks_path = os.path.join(constants.DATA_DIR, "ssconf_networks") |
193 |
sw.Write("compadd -X 'completing networks'" |
194 |
" ${(f)\"$(cat %s | xargs -n 1 | sort 2>/dev/null)\"}", |
195 |
utils.ShellQuote(networks_path)) |
196 |
finally: |
197 |
sw.DecIndent() |
198 |
sw.Write("}") |
199 |
|
200 |
sw.Write("_ganeti_nic_params() {") |
201 |
sw.IncIndent() |
202 |
try: |
203 |
sw.Write("compset -P 1 '*:'") |
204 |
sw.Write("_values -X 'nic parameters' -s , 'NIC parameters' %s" % |
205 |
" ".join(["'%s:'" % p for p in constants.INIC_PARAMS])) |
206 |
finally: |
207 |
sw.DecIndent() |
208 |
sw.Write("}") |
209 |
|
210 |
sw.Write("_ganeti_disk_params() {") |
211 |
sw.IncIndent() |
212 |
try: |
213 |
sw.Write("compset -P 1 '*:'") |
214 |
sw.Write("_values -X 'disk parameters' -s , 'disk parameters' %s" % |
215 |
" ".join(["'%s:'" % p for p in constants.IDISK_PARAMS])) |
216 |
finally: |
217 |
sw.DecIndent() |
218 |
sw.Write("}") |
219 |
|
220 |
sw.Write("_ganeti_backend_params() {") |
221 |
sw.IncIndent() |
222 |
try: |
223 |
sw.Write("_values -X 'backend parameters' -s , 'backend parameters' %s" % |
224 |
" ".join(["'%s:'" % p for p in constants.BES_PARAMETERS])) |
225 |
finally: |
226 |
sw.DecIndent() |
227 |
sw.Write("}") |
228 |
|
229 |
|
230 |
def WriteAppendix(sw): |
231 |
sw.Write("case $service in") |
232 |
sw.IncIndent() |
233 |
try: |
234 |
for scriptname in _autoconf.GNT_SCRIPTS: |
235 |
sw.Write("%s)" % scriptname) |
236 |
sw.IncIndent() |
237 |
try: |
238 |
sw.Write("%s \"$@\"" % GetFunctionName(scriptname)) |
239 |
sw.Write(";;") |
240 |
finally: |
241 |
sw.DecIndent() |
242 |
|
243 |
finally: |
244 |
sw.DecIndent() |
245 |
sw.Write("esac") |
246 |
|
247 |
# pylint: disable=W0212 |
248 |
def WriteGntCompletion(sw, scriptname, funcname, commands=None): |
249 |
"""Writes the completion code for one command. |
250 |
|
251 |
@type sw: ShellWriter |
252 |
@param sw: Script writer |
253 |
@type scriptname: string |
254 |
@param scriptname: Name of command line program |
255 |
@type funcname: string |
256 |
@param funcname: Shell function name |
257 |
@type commands: list |
258 |
@param commands: List of all subcommands in this program |
259 |
|
260 |
""" |
261 |
sw.Write("%s() {", funcname) |
262 |
sw.IncIndent() |
263 |
try: |
264 |
sw.Write("local cmds") |
265 |
sw.Write("local subcmd") |
266 |
sw.Write("""if (( CURRENT == 2 )); then""") |
267 |
sw.IncIndent() |
268 |
try: |
269 |
# Complete the command name |
270 |
sw.Write("cmds=(") |
271 |
sw.IncIndent() |
272 |
try: |
273 |
for cmd in sorted(commands.keys()): |
274 |
sw.Write(utils.ShellQuote("%s:%s" % |
275 |
(cmd, commands[cmd][-1]))) |
276 |
finally: |
277 |
sw.Write(")") |
278 |
sw.DecIndent() |
279 |
sw.Write("_describe '%s command' cmds" % scriptname) |
280 |
finally: |
281 |
sw.DecIndent() |
282 |
sw.Write("else") |
283 |
sw.IncIndent() |
284 |
try: |
285 |
sw.Write("shift words") |
286 |
sw.Write("(( CURRENT-- ))") |
287 |
sw.Write("subcmd=\"$words[1]\"") |
288 |
sw.Write("case \"$subcmd\" in") |
289 |
for cmd, (_, argdef, optdef, _, _) in commands.iteritems(): |
290 |
if not (argdef or optdef): |
291 |
continue |
292 |
|
293 |
# TODO: Group by arguments and options |
294 |
sw.Write("%s)", utils.ShellQuote(cmd)) |
295 |
sw.IncIndent() |
296 |
try: |
297 |
#CompletionWriter(optdef, argdef).WriteTo(sw) |
298 |
sw.Write("_arguments -A \"-*\" \\") |
299 |
sw.IncIndent() |
300 |
try: |
301 |
for o in optdef: |
302 |
message = "" |
303 |
desc = "" |
304 |
completer = None |
305 |
alternatives = None |
306 |
all_names = sorted(o._short_opts + o._long_opts) |
307 |
|
308 |
if len(all_names) > 1: |
309 |
opt = "(%s)" % " ".join(all_names) |
310 |
switches = [] |
311 |
for switch in all_names: |
312 |
switch_string = switch |
313 |
if o.takes_value(): |
314 |
if switch in o._long_opts: |
315 |
switch_string += "=" |
316 |
if o.action == "append": |
317 |
switch_string = "*" + switch_string |
318 |
switches.append(switch_string) |
319 |
alternatives = "{%s}" % ",".join(switches) |
320 |
|
321 |
else: |
322 |
if o.action == "append": |
323 |
# This option can be specified multiple times. |
324 |
opt = "*" |
325 |
else: |
326 |
opt = "" |
327 |
opt += o.get_opt_string() |
328 |
if o.takes_value(): |
329 |
if o.get_opt_string() in o._long_opts: |
330 |
opt += "=" |
331 |
|
332 |
if o.help: |
333 |
desc = o.help |
334 |
desc = desc.replace("[", "\\[") |
335 |
desc = desc.replace("]", "\\]") |
336 |
desc = desc.replace(":", "\\:") |
337 |
desc = "[%s]" % desc |
338 |
|
339 |
# Only static choices implemented so far (e.g. no node list) |
340 |
suggest = getattr(o, "completion_suggest", None) |
341 |
|
342 |
# our custom option type |
343 |
if o.type == "bool": |
344 |
suggest = ["yes", "no"] |
345 |
|
346 |
if not suggest: |
347 |
suggest = o.choices |
348 |
|
349 |
if o.metavar: |
350 |
message = o.metavar.lower() |
351 |
message = message.replace(">","") |
352 |
message = message.replace("<","") |
353 |
message = message.replace("[","\\[") |
354 |
message = message.replace("]","\\]") |
355 |
message = message.replace(":", "\\:") |
356 |
|
357 |
if (isinstance(suggest, (int, long)) and |
358 |
suggest in cli.OPT_COMPL_ALL): |
359 |
if suggest == cli.OPT_COMPL_MANY_NODES: |
360 |
completer = "_ganeti_many_nodes" |
361 |
elif suggest == cli.OPT_COMPL_ONE_NODE: |
362 |
completer = "_ganeti_nodes" |
363 |
elif suggest == cli.OPT_COMPL_ONE_INSTANCE: |
364 |
completer = "_ganeti_instances" |
365 |
elif suggest == cli.OPT_COMPL_ONE_OS: |
366 |
completer = "_ganeti_os" |
367 |
elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR: |
368 |
completer = "_ganeti_iallocator" |
369 |
elif suggest == cli.OPT_COMPL_INST_ADD_NODES: |
370 |
completer = "_ganeti_instance_nodes" |
371 |
elif suggest == cli.OPT_COMPL_ONE_NODEGROUP: |
372 |
completer = "_ganeti_nodegroup" |
373 |
elif suggest == cli.OPT_COMPL_ONE_NETWORK: |
374 |
completer = "_ganeti_network" |
375 |
elif suggest == cli.OPT_COMPL_NIC_PARAMS: |
376 |
completer = "_ganeti_nic_params" |
377 |
elif suggest == cli.OPT_COMPL_DISK_PARAMS: |
378 |
completer = "_ganeti_disk_params" |
379 |
elif suggest == cli.OPT_COMPL_BACKEND_PARAMS: |
380 |
completer = "_ganeti_backend_params" |
381 |
elif suggest is not None: |
382 |
completer = "(%s)" % " ".join(sorted(suggest)) |
383 |
|
384 |
if completer and not message: |
385 |
message = " " |
386 |
|
387 |
if completer: |
388 |
tail = "%s:%s:%s" % (desc, message, completer) |
389 |
else: |
390 |
tail = desc |
391 |
if o.takes_value(): |
392 |
tail += ":" |
393 |
|
394 |
if alternatives: |
395 |
opt = utils.ShellQuote(opt) |
396 |
opt += alternatives |
397 |
opt += utils.ShellQuote(tail) |
398 |
else: |
399 |
opt = utils.ShellQuote(opt + tail) |
400 |
|
401 |
sw.Write(opt + " \\") |
402 |
|
403 |
# Argument completion |
404 |
last_idx = len(argdef) - 1 |
405 |
varlen_arg_idx = None |
406 |
|
407 |
if argdef: |
408 |
for idx, arg in enumerate(argdef): |
409 |
assert arg.min is not None and arg.min >= 0 |
410 |
assert not (idx < last_idx and arg.max is None) |
411 |
|
412 |
if arg.min != arg.max or arg.max is None: |
413 |
if varlen_arg_idx is not None: |
414 |
raise Exception("Only one argument can have" |
415 |
" variable length") |
416 |
varlen_arg_idx = idx |
417 |
|
418 |
for idx, arg in enumerate(argdef): |
419 |
if isinstance(arg, cli.ArgUnknown): |
420 |
completer = "" |
421 |
elif isinstance(arg, cli.ArgSuggest): |
422 |
completer = "(%s)" % " ".join(arg.choices) |
423 |
elif isinstance(arg, cli.ArgInstance): |
424 |
completer = "_ganeti_instances" |
425 |
elif isinstance(arg, cli.ArgNode): |
426 |
completer = "_ganeti_nodes" |
427 |
elif isinstance(arg, cli.ArgGroup): |
428 |
completer = "_ganeti_nodegroup" |
429 |
elif isinstance(arg, cli.ArgNetwork): |
430 |
completer = "_ganeti_network" |
431 |
elif isinstance(arg, cli.ArgJobId): |
432 |
completer = "_ganeti_jobs" |
433 |
elif isinstance(arg, cli.ArgOs): |
434 |
completer = "_ganeti_os" |
435 |
elif isinstance(arg, cli.ArgFile): |
436 |
completer = "_files" |
437 |
elif isinstance(arg, cli.ArgCommand): |
438 |
completer = "_path_commands" |
439 |
elif isinstance(arg, cli.ArgHost): |
440 |
completer = "_hosts" |
441 |
else: |
442 |
raise Exception("Unknown argument type %r" % arg) |
443 |
|
444 |
argopt = "" |
445 |
if idx == varlen_arg_idx: |
446 |
if arg.min == 0: |
447 |
argopt = "*:: :%s" % completer |
448 |
else: |
449 |
argopt = "*: :%s" % completer |
450 |
sw.Write(utils.ShellQuote(argopt) + ' \\') |
451 |
|
452 |
else: |
453 |
for _ in range(0, arg.min): |
454 |
sw.Write(utils.ShellQuote(": :%s" % completer) + ' \\') |
455 |
for _ in range(arg.min, arg.max): |
456 |
sw.Write(utils.ShellQuote(":: :%s" % completer) + ' \\') |
457 |
|
458 |
finally: |
459 |
sw.DecIndent() |
460 |
|
461 |
finally: |
462 |
sw.DecIndent() |
463 |
|
464 |
sw.Write(";;") |
465 |
sw.Write("esac") |
466 |
finally: |
467 |
sw.DecIndent() |
468 |
sw.Write("fi") |
469 |
finally: |
470 |
sw.DecIndent() |
471 |
sw.Write("}") |
472 |
|
473 |
sw.Write("") |
474 |
#sw.Write("compdef %s %s" % (funcname, scriptname)) |
475 |
#sw.Write("") |
476 |
|
477 |
|
478 |
def GetFunctionName(name): |
479 |
return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower()) |
480 |
|
481 |
|
482 |
def GetCommands(filename, module): |
483 |
"""Returns the commands defined in a module. |
484 |
|
485 |
Aliases are also added as commands. |
486 |
|
487 |
""" |
488 |
try: |
489 |
commands = getattr(module, "commands") |
490 |
except AttributeError: |
491 |
raise Exception("Script %s doesn't have 'commands' attribute" % |
492 |
filename) |
493 |
|
494 |
# Add the implicit "--help" option |
495 |
help_option = cli.cli_option("-h", "--help", default=False, |
496 |
action="store_true", |
497 |
help="Display usage information") |
498 |
|
499 |
for name, (_, _, optdef, _, _) in commands.items(): |
500 |
if help_option not in optdef: |
501 |
optdef.append(help_option) |
502 |
for opt in cli.COMMON_OPTS: |
503 |
if opt in optdef: |
504 |
raise Exception("Common option '%s' listed for command '%s' in %s" % |
505 |
(opt, name, filename)) |
506 |
optdef.append(opt) |
507 |
|
508 |
# Use aliases |
509 |
aliases = getattr(module, "aliases", {}) |
510 |
if aliases: |
511 |
commands = commands.copy() |
512 |
for name, target in aliases.iteritems(): |
513 |
commands[name] = commands[target] |
514 |
|
515 |
return commands |
516 |
|
517 |
|
518 |
def main(): |
519 |
buf = StringIO() |
520 |
sw = utils.ShellWriter(buf) |
521 |
|
522 |
WritePreamble(sw) |
523 |
|
524 |
# gnt-* scripts |
525 |
for scriptname in _autoconf.GNT_SCRIPTS: |
526 |
filename = "scripts/%s" % scriptname |
527 |
|
528 |
WriteGntCompletion(sw, scriptname, |
529 |
GetFunctionName(scriptname), |
530 |
commands=GetCommands(filename, |
531 |
build.LoadModule(filename))) |
532 |
|
533 |
WriteAppendix(sw) |
534 |
print buf.getvalue() |
535 |
|
536 |
|
537 |
if __name__ == "__main__": |
538 |
main() |