Merge branch 'stable-2.8' into stable-2.9
[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 os.path
31 import re
32 import itertools
33 import optparse
34 from cStringIO import StringIO
35
36 from ganeti import constants
37 from ganeti import cli
38 from ganeti import utils
39 from ganeti import build
40 from ganeti import pathutils
41
42 from ganeti.tools import burnin
43
44 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
45 # making an exception here because this script is only used at build time.
46 from ganeti import _autoconf
47
48 #: Regular expression describing desired format of option names. Long names can
49 #: contain lowercase characters, numbers and dashes only.
50 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
51
52
53 def WritePreamble(sw, support_debug):
54   """Writes the script preamble.
55
56   Helper functions should be written here.
57
58   """
59   sw.Write("# This script is automatically generated at build time.")
60   sw.Write("# Do not modify manually.")
61
62   if support_debug:
63     sw.Write("_gnt_log() {")
64     sw.IncIndent()
65     try:
66       sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
67       sw.IncIndent()
68       try:
69         sw.Write("{")
70         sw.IncIndent()
71         try:
72           sw.Write("echo ---")
73           sw.Write("echo \"$@\"")
74           sw.Write("echo")
75         finally:
76           sw.DecIndent()
77         sw.Write("} >> $GANETI_COMPL_LOG")
78       finally:
79         sw.DecIndent()
80       sw.Write("fi")
81     finally:
82       sw.DecIndent()
83     sw.Write("}")
84
85   sw.Write("_ganeti_nodes() {")
86   sw.IncIndent()
87   try:
88     node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
89     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
90   finally:
91     sw.DecIndent()
92   sw.Write("}")
93
94   sw.Write("_ganeti_instances() {")
95   sw.IncIndent()
96   try:
97     instance_list_path = os.path.join(pathutils.DATA_DIR,
98                                       "ssconf_instance_list")
99     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
100   finally:
101     sw.DecIndent()
102   sw.Write("}")
103
104   sw.Write("_ganeti_jobs() {")
105   sw.IncIndent()
106   try:
107     # FIXME: this is really going into the internals of the job queue
108     sw.Write(("local jlist=($( shopt -s nullglob &&"
109               " cd %s 2>/dev/null && echo job-* || : ))"),
110              utils.ShellQuote(pathutils.QUEUE_DIR))
111     sw.Write('echo "${jlist[@]/job-/}"')
112   finally:
113     sw.DecIndent()
114   sw.Write("}")
115
116   for (fnname, paths) in [
117     ("os", pathutils.OS_SEARCH_PATH),
118     ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
119     ]:
120     sw.Write("_ganeti_%s() {", fnname)
121     sw.IncIndent()
122     try:
123       # FIXME: Make querying the master for all OSes cheap
124       for path in paths:
125         sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
126                  utils.ShellQuote(path))
127     finally:
128       sw.DecIndent()
129     sw.Write("}")
130
131   sw.Write("_ganeti_nodegroup() {")
132   sw.IncIndent()
133   try:
134     nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
135     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
136   finally:
137     sw.DecIndent()
138   sw.Write("}")
139
140   sw.Write("_ganeti_network() {")
141   sw.IncIndent()
142   try:
143     networks_path = os.path.join(pathutils.DATA_DIR, "ssconf_networks")
144     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
145   finally:
146     sw.DecIndent()
147   sw.Write("}")
148
149   # Params: <offset> <options with values> <options without values>
150   # Result variable: $first_arg_idx
151   sw.Write("_ganeti_find_first_arg() {")
152   sw.IncIndent()
153   try:
154     sw.Write("local w i")
155
156     sw.Write("first_arg_idx=")
157     sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
158     sw.IncIndent()
159     try:
160       sw.Write("w=${COMP_WORDS[$i]}")
161
162       # Skip option value
163       sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
164
165       # Skip
166       sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
167
168       # Ah, we found the first argument
169       sw.Write("else first_arg_idx=$i; break;")
170       sw.Write("fi")
171     finally:
172       sw.DecIndent()
173     sw.Write("done")
174   finally:
175     sw.DecIndent()
176   sw.Write("}")
177
178   # Params: <list of options separated by space>
179   # Input variable: $first_arg_idx
180   # Result variables: $arg_idx, $choices
181   sw.Write("_ganeti_list_options() {")
182   sw.IncIndent()
183   try:
184     sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
185     sw.IncIndent()
186     try:
187       sw.Write("arg_idx=0")
188       # Show options only if the current word starts with a dash
189       sw.Write("""if [[ "$cur" == -* ]]; then""")
190       sw.IncIndent()
191       try:
192         sw.Write("choices=$1")
193       finally:
194         sw.DecIndent()
195       sw.Write("fi")
196       sw.Write("return")
197     finally:
198       sw.DecIndent()
199     sw.Write("fi")
200
201     # Calculate position of current argument
202     sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
203     sw.Write("choices=")
204   finally:
205     sw.DecIndent()
206   sw.Write("}")
207
208   # Params: <long options with equal sign> <all options>
209   # Result variable: $optcur
210   sw.Write("_gnt_checkopt() {")
211   sw.IncIndent()
212   try:
213     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
214     sw.IncIndent()
215     try:
216       sw.Write("optcur=\"${cur#--*=}\"")
217       sw.Write("return 0")
218     finally:
219       sw.DecIndent()
220     sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
221     sw.IncIndent()
222     try:
223       sw.Write("optcur=\"$cur\"")
224       sw.Write("return 0")
225     finally:
226       sw.DecIndent()
227     sw.Write("fi")
228
229     if support_debug:
230       sw.Write("_gnt_log optcur=\"'$optcur'\"")
231
232     sw.Write("return 1")
233   finally:
234     sw.DecIndent()
235   sw.Write("}")
236
237   # Params: <compgen options>
238   # Result variable: $COMPREPLY
239   sw.Write("_gnt_compgen() {")
240   sw.IncIndent()
241   try:
242     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
243     if support_debug:
244       sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
245   finally:
246     sw.DecIndent()
247   sw.Write("}")
248
249
250 def WriteCompReply(sw, args, cur="\"$cur\""):
251   sw.Write("_gnt_compgen %s -- %s", args, cur)
252   sw.Write("return")
253
254
255 class CompletionWriter:
256   """Command completion writer class.
257
258   """
259   def __init__(self, arg_offset, opts, args, support_debug):
260     self.arg_offset = arg_offset
261     self.opts = opts
262     self.args = args
263     self.support_debug = support_debug
264
265     for opt in opts:
266       # While documented, these variables aren't seen as public attributes by
267       # pylint. pylint: disable=W0212
268       opt.all_names = sorted(opt._short_opts + opt._long_opts)
269
270       invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
271       if invalid:
272         raise Exception("Option names don't match regular expression '%s': %s" %
273                         (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
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       # our custom option type
316       if opt.type == "bool":
317         suggest = ["yes", "no"]
318
319       if not suggest:
320         suggest = opt.choices
321
322       if (isinstance(suggest, (int, long)) and
323           suggest in cli.OPT_COMPL_ALL):
324         key = suggest
325       elif suggest:
326         key = " ".join(sorted(suggest))
327       else:
328         key = ""
329
330       values.setdefault(key, []).extend(opt.all_names)
331
332     # Don't write any code if there are no option values
333     if not values:
334       return
335
336     cur = "\"$optcur\""
337
338     wrote_opt = False
339
340     for (suggest, allnames) in values.items():
341       longnames = [i for i in allnames if i.startswith("--")]
342
343       if wrote_opt:
344         condcmd = "elif"
345       else:
346         condcmd = "if"
347
348       sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
349                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
350                utils.ShellQuote("|".join(allnames)))
351       sw.IncIndent()
352       try:
353         if suggest == cli.OPT_COMPL_MANY_NODES:
354           # TODO: Implement comma-separated values
355           WriteCompReply(sw, "-W ''", cur=cur)
356         elif suggest == cli.OPT_COMPL_ONE_NODE:
357           WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
358         elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
359           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
360         elif suggest == cli.OPT_COMPL_ONE_OS:
361           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
362         elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
363           WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
364         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
365           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
366         elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
367           WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
368         elif suggest == cli.OPT_COMPL_ONE_NETWORK:
369           WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
370         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
371           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
372
373           sw.Write("if [[ \"$optcur\" == *:* ]]; then")
374           sw.IncIndent()
375           try:
376             sw.Write("node1=\"${optcur%%:*}\"")
377
378             sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
379             sw.IncIndent()
380             try:
381               sw.Write("pfx=\"$node1:\"")
382             finally:
383               sw.DecIndent()
384             sw.Write("fi")
385           finally:
386             sw.DecIndent()
387           sw.Write("fi")
388
389           if self.support_debug:
390             sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
391                      " node1=\"'$node1'\"")
392
393           sw.Write("for i in $(_ganeti_nodes); do")
394           sw.IncIndent()
395           try:
396             sw.Write("if [[ -z \"$node1\" ]]; then")
397             sw.IncIndent()
398             try:
399               sw.Write("tmp=\"$tmp $i $i:\"")
400             finally:
401               sw.DecIndent()
402             sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
403             sw.IncIndent()
404             try:
405               sw.Write("tmp=\"$tmp $i\"")
406             finally:
407               sw.DecIndent()
408             sw.Write("fi")
409           finally:
410             sw.DecIndent()
411           sw.Write("done")
412
413           WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
414         else:
415           WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
416       finally:
417         sw.DecIndent()
418
419       wrote_opt = True
420
421     if wrote_opt:
422       sw.Write("fi")
423
424     return
425
426   def _CompleteArguments(self, sw):
427     if not (self.opts or self.args):
428       return
429
430     all_option_names = []
431     for opt in self.opts:
432       all_option_names.extend(opt.all_names)
433     all_option_names.sort()
434
435     # List options if no argument has been specified yet
436     sw.Write("_ganeti_list_options %s",
437              utils.ShellQuote(" ".join(all_option_names)))
438
439     if self.args:
440       last_idx = len(self.args) - 1
441       last_arg_end = 0
442       varlen_arg_idx = None
443       wrote_arg = False
444
445       sw.Write("compgenargs=")
446
447       for idx, arg in enumerate(self.args):
448         assert arg.min is not None and arg.min >= 0
449         assert not (idx < last_idx and arg.max is None)
450
451         if arg.min != arg.max or arg.max is None:
452           if varlen_arg_idx is not None:
453             raise Exception("Only one argument can have a variable length")
454           varlen_arg_idx = idx
455
456         compgenargs = []
457
458         if isinstance(arg, cli.ArgUnknown):
459           choices = ""
460         elif isinstance(arg, cli.ArgSuggest):
461           choices = utils.ShellQuote(" ".join(arg.choices))
462         elif isinstance(arg, cli.ArgInstance):
463           choices = "$(_ganeti_instances)"
464         elif isinstance(arg, cli.ArgNode):
465           choices = "$(_ganeti_nodes)"
466         elif isinstance(arg, cli.ArgGroup):
467           choices = "$(_ganeti_nodegroup)"
468         elif isinstance(arg, cli.ArgNetwork):
469           choices = "$(_ganeti_network)"
470         elif isinstance(arg, cli.ArgJobId):
471           choices = "$(_ganeti_jobs)"
472         elif isinstance(arg, cli.ArgOs):
473           choices = "$(_ganeti_os)"
474         elif isinstance(arg, cli.ArgExtStorage):
475           choices = "$(_ganeti_extstorage)"
476         elif isinstance(arg, cli.ArgFile):
477           choices = ""
478           compgenargs.append("-f")
479         elif isinstance(arg, cli.ArgCommand):
480           choices = ""
481           compgenargs.append("-c")
482         elif isinstance(arg, cli.ArgHost):
483           choices = ""
484           compgenargs.append("-A hostname")
485         else:
486           raise Exception("Unknown argument type %r" % arg)
487
488         if arg.min == 1 and arg.max == 1:
489           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
490         elif arg.max is None:
491           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
492         elif arg.min <= arg.max:
493           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
494                      (last_arg_end, last_arg_end + arg.max))
495         else:
496           raise Exception("Unable to generate argument position condition")
497
498         last_arg_end += arg.min
499
500         if choices or compgenargs:
501           if wrote_arg:
502             condcmd = "elif"
503           else:
504             condcmd = "if"
505
506           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
507           sw.IncIndent()
508           try:
509             if choices:
510               sw.Write("""choices="$choices "%s""", choices)
511             if compgenargs:
512               sw.Write("compgenargs=%s",
513                        utils.ShellQuote(" ".join(compgenargs)))
514           finally:
515             sw.DecIndent()
516
517           wrote_arg = True
518
519       if wrote_arg:
520         sw.Write("fi")
521
522     if self.args:
523       WriteCompReply(sw, """-W "$choices" $compgenargs""")
524     else:
525       # $compgenargs exists only if there are arguments
526       WriteCompReply(sw, '-W "$choices"')
527
528   def WriteTo(self, sw):
529     self._FindFirstArgument(sw)
530     self._CompleteOptionValues(sw)
531     self._CompleteArguments(sw)
532
533
534 def WriteCompletion(sw, scriptname, funcname, support_debug,
535                     commands=None,
536                     opts=None, args=None):
537   """Writes the completion code for one command.
538
539   @type sw: ShellWriter
540   @param sw: Script writer
541   @type scriptname: string
542   @param scriptname: Name of command line program
543   @type funcname: string
544   @param funcname: Shell function name
545   @type commands: list
546   @param commands: List of all subcommands in this program
547
548   """
549   sw.Write("%s() {", funcname)
550   sw.IncIndent()
551   try:
552     sw.Write("local "
553              ' cur="${COMP_WORDS[COMP_CWORD]}"'
554              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
555              ' i first_arg_idx choices compgenargs arg_idx optcur')
556
557     if support_debug:
558       sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
559       sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
560                " _gnt_log \"$(set | grep ^COMP_)\"")
561
562     sw.Write("COMPREPLY=()")
563
564     if opts is not None and args is not None:
565       assert not commands
566       CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
567
568     else:
569       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
570       sw.IncIndent()
571       try:
572         # Complete the command name
573         WriteCompReply(sw,
574                        ("-W %s" %
575                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
576       finally:
577         sw.DecIndent()
578       sw.Write("fi")
579
580       # Group commands by arguments and options
581       grouped_cmds = {}
582       for cmd, (_, argdef, optdef, _, _) in commands.items():
583         if not (argdef or optdef):
584           continue
585         grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
586
587       # We're doing options and arguments to commands
588       sw.Write("""case "${COMP_WORDS[1]}" in""")
589       sort_grouped = sorted(grouped_cmds.items(),
590                             key=lambda (_, y): sorted(y)[0])
591       for ((argdef, optdef), cmds) in sort_grouped:
592         assert argdef or optdef
593         sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
594         sw.IncIndent()
595         try:
596           CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
597         finally:
598           sw.DecIndent()
599         sw.Write(";;")
600       sw.Write("esac")
601   finally:
602     sw.DecIndent()
603   sw.Write("}")
604
605   sw.Write("complete -F %s -o filenames %s",
606            utils.ShellQuote(funcname),
607            utils.ShellQuote(scriptname))
608
609
610 def GetFunctionName(name):
611   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
612
613
614 def GetCommands(filename, module):
615   """Returns the commands defined in a module.
616
617   Aliases are also added as commands.
618
619   """
620   try:
621     commands = getattr(module, "commands")
622   except AttributeError:
623     raise Exception("Script %s doesn't have 'commands' attribute" %
624                     filename)
625
626   # Add the implicit "--help" option
627   help_option = cli.cli_option("-h", "--help", default=False,
628                                action="store_true")
629
630   for name, (_, _, optdef, _, _) in commands.items():
631     if help_option not in optdef:
632       optdef.append(help_option)
633     for opt in cli.COMMON_OPTS:
634       if opt in optdef:
635         raise Exception("Common option '%s' listed for command '%s' in %s" %
636                         (opt, name, filename))
637       optdef.append(opt)
638
639   # Use aliases
640   aliases = getattr(module, "aliases", {})
641   if aliases:
642     commands = commands.copy()
643     for name, target in aliases.items():
644       commands[name] = commands[target]
645
646   return commands
647
648
649 def HaskellOptToOptParse(opts, kind):
650   """Converts a Haskell options to Python cli_options.
651
652   @type opts: string
653   @param opts: comma-separated string with short and long options
654   @type kind: string
655   @param kind: type generated by Common.hs/complToText; needs to be
656       kept in sync
657
658   """
659   # pylint: disable=W0142
660   # since we pass *opts in a number of places
661   opts = opts.split(",")
662   if kind == "none":
663     return cli.cli_option(*opts, action="store_true")
664   elif kind in ["file", "string", "host", "dir", "inetaddr"]:
665     return cli.cli_option(*opts, type="string")
666   elif kind == "integer":
667     return cli.cli_option(*opts, type="int")
668   elif kind == "float":
669     return cli.cli_option(*opts, type="float")
670   elif kind == "onegroup":
671     return cli.cli_option(*opts, type="string",
672                            completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
673   elif kind == "onenode":
674     return cli.cli_option(*opts, type="string",
675                           completion_suggest=cli.OPT_COMPL_ONE_NODE)
676   elif kind == "manyinstances":
677     # FIXME: no support for many instances
678     return cli.cli_option(*opts, type="string")
679   elif kind.startswith("choices="):
680     choices = kind[len("choices="):].split(",")
681     return cli.cli_option(*opts, type="choice", choices=choices)
682   else:
683     # FIXME: there are many other currently unused completion types,
684     # should be added on an as-needed basis
685     raise Exception("Unhandled option kind '%s'" % kind)
686
687
688 #: serialised kind to arg type
689 _ARG_MAP = {
690   "choices": cli.ArgChoice,
691   "command": cli.ArgCommand,
692   "file": cli.ArgFile,
693   "host": cli.ArgHost,
694   "jobid": cli.ArgJobId,
695   "onegroup": cli.ArgGroup,
696   "oneinstance": cli.ArgInstance,
697   "onenode": cli.ArgNode,
698   "oneos": cli.ArgOs,
699   "string": cli.ArgUnknown,
700   "suggests": cli.ArgSuggest,
701   }
702
703
704 def HaskellArgToCliArg(kind, min_cnt, max_cnt):
705   """Converts a Haskell options to Python _Argument.
706
707   @type kind: string
708   @param kind: type generated by Common.hs/argComplToText; needs to be
709       kept in sync
710
711   """
712   min_cnt = int(min_cnt)
713   if max_cnt == "none":
714     max_cnt = None
715   else:
716     max_cnt = int(max_cnt)
717   # pylint: disable=W0142
718   # since we pass **kwargs
719   kwargs = {"min": min_cnt, "max": max_cnt}
720
721   if kind.startswith("choices=") or kind.startswith("suggest="):
722     (kind, choices) = kind.split("=", 1)
723     kwargs["choices"] = choices.split(",")
724
725   if kind not in _ARG_MAP:
726     raise Exception("Unhandled argument kind '%s'" % kind)
727   else:
728     return _ARG_MAP[kind](**kwargs)
729
730
731 def ParseHaskellOptsArgs(script, output):
732   """Computes list of options/arguments from help-completion output.
733
734   """
735   cli_opts = []
736   cli_args = []
737   for line in output.splitlines():
738     v = line.split(None)
739     exc = lambda msg: Exception("Invalid %s output from %s: %s" %
740                                 (msg, script, v))
741     if len(v) < 2:
742       raise exc("help completion")
743     if v[0].startswith("-"):
744       if len(v) != 2:
745         raise exc("option format")
746       (opts, kind) = v
747       cli_opts.append(HaskellOptToOptParse(opts, kind))
748     else:
749       if len(v) != 3:
750         raise exc("argument format")
751       (kind, min_cnt, max_cnt) = v
752       cli_args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
753   return (cli_opts, cli_args)
754
755
756 def WriteHaskellCompletion(sw, script, htools=True, debug=True):
757   """Generates completion information for a Haskell program.
758
759   This converts completion info from a Haskell program into 'fake'
760   cli_opts and then builds completion for them.
761
762   """
763   if htools:
764     cmd = "./src/htools"
765     env = {"HTOOLS": script}
766     script_name = script
767     func_name = "htools_%s" % script
768   else:
769     cmd = "./" + script
770     env = {}
771     script_name = os.path.basename(script)
772     func_name = script_name
773   func_name = GetFunctionName(func_name)
774   output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
775   (opts, args) = ParseHaskellOptsArgs(script_name, output)
776   WriteCompletion(sw, script_name, func_name, debug, opts=opts, args=args)
777
778
779 def WriteHaskellCmdCompletion(sw, script, debug=True):
780   """Generates completion information for a Haskell multi-command program.
781
782   This gathers the list of commands from a Haskell program and
783   computes the list of commands available, then builds the sub-command
784   list of options/arguments for each command, using that for building
785   a unified help output.
786
787   """
788   cmd = "./" + script
789   script_name = os.path.basename(script)
790   func_name = script_name
791   func_name = GetFunctionName(func_name)
792   output = utils.RunCmd([cmd, "--help-completion"], cwd=".").output
793   commands = {}
794   lines = output.splitlines()
795   if len(lines) != 1:
796     raise Exception("Invalid lines in multi-command mode: %s" % str(lines))
797   v = lines[0].split(None)
798   exc = lambda msg: Exception("Invalid %s output from %s: %s" %
799                               (msg, script, v))
800   if len(v) != 3:
801     raise exc("help completion in multi-command mode")
802   if not v[0].startswith("choices="):
803     raise exc("invalid format in multi-command mode '%s'" % v[0])
804   for subcmd in v[0][len("choices="):].split(","):
805     output = utils.RunCmd([cmd, subcmd, "--help-completion"], cwd=".").output
806     (opts, args) = ParseHaskellOptsArgs(script, output)
807     commands[subcmd] = (None, args, opts, None, None)
808   WriteCompletion(sw, script_name, func_name, debug, commands=commands)
809
810
811 def main():
812   parser = optparse.OptionParser(usage="%prog [--compact]")
813   parser.add_option("--compact", action="store_true",
814                     help=("Don't indent output and don't include debugging"
815                           " facilities"))
816
817   options, args = parser.parse_args()
818   if args:
819     parser.error("Wrong number of arguments")
820
821   # Whether to build debug version of completion script
822   debug = not options.compact
823
824   buf = StringIO()
825   sw = utils.ShellWriter(buf, indent=debug)
826
827   # Remember original state of extglob and enable it (required for pattern
828   # matching; must be enabled while parsing script)
829   sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
830   sw.Write("shopt -s extglob")
831
832   WritePreamble(sw, debug)
833
834   # gnt-* scripts
835   for scriptname in _autoconf.GNT_SCRIPTS:
836     filename = "scripts/%s" % scriptname
837
838     WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
839                     commands=GetCommands(filename,
840                                          build.LoadModule(filename)))
841
842   # Burnin script
843   WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
844                   debug,
845                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
846
847   # ganeti-cleaner
848   WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
849                          debug=not options.compact)
850
851   # htools, if enabled
852   if _autoconf.HTOOLS:
853     for script in _autoconf.HTOOLS_PROGS:
854       WriteHaskellCompletion(sw, script, htools=True, debug=debug)
855
856   # ganeti-confd, if enabled
857   if _autoconf.ENABLE_CONFD:
858     WriteHaskellCompletion(sw, "src/ganeti-confd", htools=False,
859                            debug=debug)
860
861   # mon-collector, if monitoring is enabled
862   if _autoconf.ENABLE_MOND:
863     WriteHaskellCmdCompletion(sw, "src/mon-collector", debug=debug)
864
865   # Reset extglob to original value
866   sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
867   sw.Write("unset gnt_shopt_extglob")
868
869   print buf.getvalue()
870
871
872 if __name__ == "__main__":
873   main()