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