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