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