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