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