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