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