Merge branch 'devel-2.3'
[ganeti-local] / autotools / build-bash-completion
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2009 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 bash_completion script for Ganeti.
23
24 """
25
26 # pylint: disable-msg=C0103
27 # [C0103] Invalid name build-bash-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
43 def WritePreamble(sw):
44   """Writes the script preamble.
45
46   Helper functions should be written here.
47
48   """
49   sw.Write("# This script is automatically generated at build time.")
50   sw.Write("# Do not modify manually.")
51
52   sw.Write("_ganeti_dbglog() {")
53   sw.IncIndent()
54   try:
55     sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
56     sw.IncIndent()
57     try:
58       sw.Write("{")
59       sw.IncIndent()
60       try:
61         sw.Write("echo ---")
62         sw.Write("echo \"$@\"")
63         sw.Write("echo")
64       finally:
65         sw.DecIndent()
66       sw.Write("} >> $GANETI_COMPL_LOG")
67     finally:
68       sw.DecIndent()
69     sw.Write("fi")
70   finally:
71     sw.DecIndent()
72   sw.Write("}")
73
74   sw.Write("_ganeti_nodes() {")
75   sw.IncIndent()
76   try:
77     node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
78     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
79   finally:
80     sw.DecIndent()
81   sw.Write("}")
82
83   sw.Write("_ganeti_instances() {")
84   sw.IncIndent()
85   try:
86     instance_list_path = os.path.join(constants.DATA_DIR,
87                                       "ssconf_instance_list")
88     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
89   finally:
90     sw.DecIndent()
91   sw.Write("}")
92
93   sw.Write("_ganeti_jobs() {")
94   sw.IncIndent()
95   try:
96     # FIXME: this is really going into the internals of the job queue
97     sw.Write(("local jlist=$( shopt -s nullglob &&"
98               " cd %s 2>/dev/null && echo job-* || : )"),
99              utils.ShellQuote(constants.QUEUE_DIR))
100     sw.Write('echo "${jlist//job-/}"')
101   finally:
102     sw.DecIndent()
103   sw.Write("}")
104
105   for (fnname, paths) in [
106       ("os", constants.OS_SEARCH_PATH),
107       ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
108       ]:
109     sw.Write("_ganeti_%s() {", fnname)
110     sw.IncIndent()
111     try:
112       # FIXME: Make querying the master for all OSes cheap
113       for path in paths:
114         sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
115                  utils.ShellQuote(path))
116     finally:
117       sw.DecIndent()
118     sw.Write("}")
119
120   sw.Write("_ganeti_nodegroup() {")
121   sw.IncIndent()
122   try:
123     nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
124     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
125   finally:
126     sw.DecIndent()
127   sw.Write("}")
128
129   # Params: <offset> <options with values> <options without values>
130   # Result variable: $first_arg_idx
131   sw.Write("_ganeti_find_first_arg() {")
132   sw.IncIndent()
133   try:
134     sw.Write("local w i")
135
136     sw.Write("first_arg_idx=")
137     sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
138     sw.IncIndent()
139     try:
140       sw.Write("w=${COMP_WORDS[$i]}")
141
142       # Skip option value
143       sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
144
145       # Skip
146       sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
147
148       # Ah, we found the first argument
149       sw.Write("else first_arg_idx=$i; break;")
150       sw.Write("fi")
151     finally:
152       sw.DecIndent()
153     sw.Write("done")
154   finally:
155     sw.DecIndent()
156   sw.Write("}")
157
158   # Params: <list of options separated by space>
159   # Input variable: $first_arg_idx
160   # Result variables: $arg_idx, $choices
161   sw.Write("_ganeti_list_options() {")
162   sw.IncIndent()
163   try:
164     sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
165     sw.IncIndent()
166     try:
167       sw.Write("arg_idx=0")
168       # Show options only if the current word starts with a dash
169       sw.Write("""if [[ "$cur" == -* ]]; then""")
170       sw.IncIndent()
171       try:
172         sw.Write("choices=$1")
173       finally:
174         sw.DecIndent()
175       sw.Write("fi")
176       sw.Write("return")
177     finally:
178       sw.DecIndent()
179     sw.Write("fi")
180
181     # Calculate position of current argument
182     sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
183     sw.Write("choices=")
184   finally:
185     sw.DecIndent()
186   sw.Write("}")
187
188   # Params: <long options with equal sign> <all options>
189   # Result variable: $optcur
190   sw.Write("_ganeti_checkopt() {")
191   sw.IncIndent()
192   try:
193     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
194     sw.IncIndent()
195     try:
196       sw.Write("optcur=\"${cur#--*=}\"")
197       sw.Write("return 0")
198     finally:
199       sw.DecIndent()
200     sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
201     sw.IncIndent()
202     try:
203       sw.Write("optcur=\"$cur\"")
204       sw.Write("return 0")
205     finally:
206       sw.DecIndent()
207     sw.Write("fi")
208
209     sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
210
211     sw.Write("return 1")
212   finally:
213     sw.DecIndent()
214   sw.Write("}")
215
216   # Params: <compgen options>
217   # Result variable: $COMPREPLY
218   sw.Write("_ganeti_compgen() {")
219   sw.IncIndent()
220   try:
221     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
222     sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
223   finally:
224     sw.DecIndent()
225   sw.Write("}")
226
227
228 def WriteCompReply(sw, args, cur="\"$cur\""):
229   sw.Write("_ganeti_compgen %s -- %s", args, cur)
230   sw.Write("return")
231
232
233 class CompletionWriter:
234   """Command completion writer class.
235
236   """
237   def __init__(self, arg_offset, opts, args):
238     self.arg_offset = arg_offset
239     self.opts = opts
240     self.args = args
241
242     for opt in opts:
243       # While documented, these variables aren't seen as public attributes by
244       # pylint. pylint: disable-msg=W0212
245       opt.all_names = sorted(opt._short_opts + opt._long_opts)
246
247   def _FindFirstArgument(self, sw):
248     ignore = []
249     skip_one = []
250
251     for opt in self.opts:
252       if opt.takes_value():
253         # Ignore value
254         for i in opt.all_names:
255           if i.startswith("--"):
256             ignore.append("%s=*" % utils.ShellQuote(i))
257           skip_one.append(utils.ShellQuote(i))
258       else:
259         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
260
261     ignore = sorted(utils.UniqueSequence(ignore))
262     skip_one = sorted(utils.UniqueSequence(skip_one))
263
264     if ignore or skip_one:
265       # Try to locate first argument
266       sw.Write("_ganeti_find_first_arg %s %s %s",
267                self.arg_offset + 1,
268                utils.ShellQuote("|".join(skip_one)),
269                utils.ShellQuote("|".join(ignore)))
270     else:
271       # When there are no options the first argument is always at position
272       # offset + 1
273       sw.Write("first_arg_idx=%s", self.arg_offset + 1)
274
275   def _CompleteOptionValues(self, sw):
276     # Group by values
277     # "values" -> [optname1, optname2, ...]
278     values = {}
279
280     for opt in self.opts:
281       if not opt.takes_value():
282         continue
283
284       # Only static choices implemented so far (e.g. no node list)
285       suggest = getattr(opt, "completion_suggest", None)
286
287       # our custom option type
288       if opt.type == "bool":
289         suggest = ["yes", "no"]
290
291       if not suggest:
292         suggest = opt.choices
293
294       if (isinstance(suggest, (int, long)) and
295           suggest in cli.OPT_COMPL_ALL):
296         key = suggest
297       elif suggest:
298         key = " ".join(sorted(suggest))
299       else:
300         key = ""
301
302       values.setdefault(key, []).extend(opt.all_names)
303
304     # Don't write any code if there are no option values
305     if not values:
306       return
307
308     cur = "\"$optcur\""
309
310     wrote_opt = False
311
312     for (suggest, allnames) in values.iteritems():
313       longnames = [i for i in allnames if i.startswith("--")]
314
315       if wrote_opt:
316         condcmd = "elif"
317       else:
318         condcmd = "if"
319
320       sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
321                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
322                utils.ShellQuote("|".join(allnames)))
323       sw.IncIndent()
324       try:
325         if suggest == cli.OPT_COMPL_MANY_NODES:
326           # TODO: Implement comma-separated values
327           WriteCompReply(sw, "-W ''", cur=cur)
328         elif suggest == cli.OPT_COMPL_ONE_NODE:
329           WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
330         elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
331           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
332         elif suggest == cli.OPT_COMPL_ONE_OS:
333           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
334         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
335           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
336         elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
337           WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
338         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
339           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
340
341           sw.Write("if [[ \"$optcur\" == *:* ]]; then")
342           sw.IncIndent()
343           try:
344             sw.Write("node1=\"${optcur%%:*}\"")
345
346             sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
347             sw.IncIndent()
348             try:
349               sw.Write("pfx=\"$node1:\"")
350             finally:
351               sw.DecIndent()
352             sw.Write("fi")
353           finally:
354             sw.DecIndent()
355           sw.Write("fi")
356
357           sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
358                    " node1=\"'$node1'\"")
359
360           sw.Write("for i in $(_ganeti_nodes); do")
361           sw.IncIndent()
362           try:
363             sw.Write("if [[ -z \"$node1\" ]]; then")
364             sw.IncIndent()
365             try:
366               sw.Write("tmp=\"$tmp $i $i:\"")
367             finally:
368               sw.DecIndent()
369             sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
370             sw.IncIndent()
371             try:
372               sw.Write("tmp=\"$tmp $i\"")
373             finally:
374               sw.DecIndent()
375             sw.Write("fi")
376           finally:
377             sw.DecIndent()
378           sw.Write("done")
379
380           WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
381         else:
382           WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
383       finally:
384         sw.DecIndent()
385
386       wrote_opt = True
387
388     if wrote_opt:
389       sw.Write("fi")
390
391     return
392
393   def _CompleteArguments(self, sw):
394     if not (self.opts or self.args):
395       return
396
397     all_option_names = []
398     for opt in self.opts:
399       all_option_names.extend(opt.all_names)
400     all_option_names.sort()
401
402     # List options if no argument has been specified yet
403     sw.Write("_ganeti_list_options %s",
404              utils.ShellQuote(" ".join(all_option_names)))
405
406     if self.args:
407       last_idx = len(self.args) - 1
408       last_arg_end = 0
409       varlen_arg_idx = None
410       wrote_arg = False
411
412       # Write some debug comments
413       for idx, arg in enumerate(self.args):
414         sw.Write("# %s: %r", idx, arg)
415
416       sw.Write("compgenargs=")
417
418       for idx, arg in enumerate(self.args):
419         assert arg.min is not None and arg.min >= 0
420         assert not (idx < last_idx and arg.max is None)
421
422         if arg.min != arg.max or arg.max is None:
423           if varlen_arg_idx is not None:
424             raise Exception("Only one argument can have a variable length")
425           varlen_arg_idx = idx
426
427         compgenargs = []
428
429         if isinstance(arg, cli.ArgUnknown):
430           choices = ""
431         elif isinstance(arg, cli.ArgSuggest):
432           choices = utils.ShellQuote(" ".join(arg.choices))
433         elif isinstance(arg, cli.ArgInstance):
434           choices = "$(_ganeti_instances)"
435         elif isinstance(arg, cli.ArgNode):
436           choices = "$(_ganeti_nodes)"
437         elif isinstance(arg, cli.ArgJobId):
438           choices = "$(_ganeti_jobs)"
439         elif isinstance(arg, cli.ArgOs):
440           choices = "$(_ganeti_os)"
441         elif isinstance(arg, cli.ArgFile):
442           choices = ""
443           compgenargs.append("-f")
444         elif isinstance(arg, cli.ArgCommand):
445           choices = ""
446           compgenargs.append("-c")
447         elif isinstance(arg, cli.ArgHost):
448           choices = ""
449           compgenargs.append("-A hostname")
450         else:
451           raise Exception("Unknown argument type %r" % arg)
452
453         if arg.min == 1 and arg.max == 1:
454           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
455         elif arg.max is None:
456           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
457         elif arg.min <= arg.max:
458           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
459                      (last_arg_end, last_arg_end + arg.max))
460         else:
461           raise Exception("Unable to generate argument position condition")
462
463         last_arg_end += arg.min
464
465         if choices or compgenargs:
466           if wrote_arg:
467             condcmd = "elif"
468           else:
469             condcmd = "if"
470
471           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
472           sw.IncIndent()
473           try:
474             if choices:
475               sw.Write("""choices="$choices "%s""", choices)
476             if compgenargs:
477               sw.Write("compgenargs=%s",
478                        utils.ShellQuote(" ".join(compgenargs)))
479           finally:
480             sw.DecIndent()
481
482           wrote_arg = True
483
484       if wrote_arg:
485         sw.Write("fi")
486
487     if self.args:
488       WriteCompReply(sw, """-W "$choices" $compgenargs""")
489     else:
490       # $compgenargs exists only if there are arguments
491       WriteCompReply(sw, '-W "$choices"')
492
493   def WriteTo(self, sw):
494     self._FindFirstArgument(sw)
495     self._CompleteOptionValues(sw)
496     self._CompleteArguments(sw)
497
498
499 def WriteCompletion(sw, scriptname, funcname,
500                     commands=None,
501                     opts=None, args=None):
502   """Writes the completion code for one command.
503
504   @type sw: ShellWriter
505   @param sw: Script writer
506   @type scriptname: string
507   @param scriptname: Name of command line program
508   @type funcname: string
509   @param funcname: Shell function name
510   @type commands: list
511   @param commands: List of all subcommands in this program
512
513   """
514   sw.Write("%s() {", funcname)
515   sw.IncIndent()
516   try:
517     sw.Write("local "
518              ' cur="${COMP_WORDS[COMP_CWORD]}"'
519              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
520              ' i first_arg_idx choices compgenargs arg_idx optcur')
521
522     sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
523     sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
524              " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
525
526     sw.Write("COMPREPLY=()")
527
528     if opts is not None and args is not None:
529       assert not commands
530       CompletionWriter(0, opts, args).WriteTo(sw)
531
532     else:
533       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
534       sw.IncIndent()
535       try:
536         # Complete the command name
537         WriteCompReply(sw,
538                        ("-W %s" %
539                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
540       finally:
541         sw.DecIndent()
542       sw.Write("fi")
543
544       # We're doing options and arguments to commands
545       sw.Write("""case "${COMP_WORDS[1]}" in""")
546       for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
547         if not (argdef or optdef):
548           continue
549
550         # TODO: Group by arguments and options
551         sw.Write("%s)", utils.ShellQuote(cmd))
552         sw.IncIndent()
553         try:
554           CompletionWriter(1, optdef, argdef).WriteTo(sw)
555         finally:
556           sw.DecIndent()
557
558         sw.Write(";;")
559       sw.Write("esac")
560   finally:
561     sw.DecIndent()
562   sw.Write("}")
563
564   sw.Write("complete -F %s -o filenames %s",
565            utils.ShellQuote(funcname),
566            utils.ShellQuote(scriptname))
567
568
569 def GetFunctionName(name):
570   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
571
572
573 def GetCommands(filename, module):
574   """Returns the commands defined in a module.
575
576   Aliases are also added as commands.
577
578   """
579   try:
580     commands = getattr(module, "commands")
581   except AttributeError:
582     raise Exception("Script %s doesn't have 'commands' attribute" %
583                     filename)
584
585   # Add the implicit "--help" option
586   help_option = cli.cli_option("-h", "--help", default=False,
587                                action="store_true")
588
589   for name, (_, _, optdef, _, _) in commands.items():
590     if help_option not in optdef:
591       optdef.append(help_option)
592     for opt in cli.COMMON_OPTS:
593       if opt in optdef:
594         raise Exception("Common option '%s' listed for command '%s' in %s" %
595                         (opt, name, filename))
596       optdef.append(opt)
597
598   # Use aliases
599   aliases = getattr(module, "aliases", {})
600   if aliases:
601     commands = commands.copy()
602     for name, target in aliases.iteritems():
603       commands[name] = commands[target]
604
605   return commands
606
607
608 def main():
609   buf = StringIO()
610   sw = utils.ShellWriter(buf)
611
612   WritePreamble(sw)
613
614   # gnt-* scripts
615   for scriptname in _autoconf.GNT_SCRIPTS:
616     filename = "scripts/%s" % scriptname
617
618     WriteCompletion(sw, scriptname,
619                     GetFunctionName(scriptname),
620                     commands=GetCommands(filename,
621                                          build.LoadModule(filename)))
622
623   # Burnin script
624   burnin = build.LoadModule("tools/burnin")
625   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
626                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
627
628   print buf.getvalue()
629
630
631 if __name__ == "__main__":
632   main()