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