build-bash-completion: Check for None before comparing
[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   # Params: <compgen options>
240   # Result variable: $COMPREPLY
241   sw.Write("_ganeti_compgen() {")
242   sw.IncIndent()
243   try:
244     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
245     sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
246   finally:
247     sw.DecIndent()
248   sw.Write("}")
249
250
251 def WriteCompReply(sw, args, cur="\"$cur\""):
252   sw.Write("_ganeti_compgen %s -- %s", args, cur)
253   sw.Write("return")
254
255
256 class CompletionWriter:
257   """Command completion writer class.
258
259   """
260   def __init__(self, arg_offset, opts, args):
261     self.arg_offset = arg_offset
262     self.opts = opts
263     self.args = args
264
265     for opt in opts:
266       opt.all_names = sorted(opt._short_opts + opt._long_opts)
267
268   def _FindFirstArgument(self, sw):
269     ignore = []
270     skip_one = []
271
272     for opt in self.opts:
273       if opt.takes_value():
274         # Ignore value
275         for i in opt.all_names:
276           if i.startswith("--"):
277             ignore.append("%s=*" % utils.ShellQuote(i))
278           skip_one.append(utils.ShellQuote(i))
279       else:
280         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
281
282     ignore = sorted(utils.UniqueSequence(ignore))
283     skip_one = sorted(utils.UniqueSequence(skip_one))
284
285     if ignore or skip_one:
286       # Try to locate first argument
287       sw.Write("_ganeti_find_first_arg %s %s %s",
288                self.arg_offset + 1,
289                utils.ShellQuote("|".join(skip_one)),
290                utils.ShellQuote("|".join(ignore)))
291     else:
292       # When there are no options the first argument is always at position
293       # offset + 1
294       sw.Write("first_arg_idx=%s", self.arg_offset + 1)
295
296   def _CompleteOptionValues(self, sw):
297     # Group by values
298     # "values" -> [optname1, optname2, ...]
299     values = {}
300
301     for opt in self.opts:
302       if not opt.takes_value():
303         continue
304
305       # Only static choices implemented so far (e.g. no node list)
306       suggest = getattr(opt, "completion_suggest", None)
307
308       if not suggest:
309         suggest = opt.choices
310
311       if (isinstance(suggest, (int, long)) and
312           suggest in cli.OPT_COMPL_ALL):
313         key = suggest
314       elif suggest:
315         key = " ".join(sorted(suggest))
316       else:
317         key = ""
318
319       values.setdefault(key, []).extend(opt.all_names)
320
321     # Don't write any code if there are no option values
322     if not values:
323       return
324
325     cur = "\"$optcur\""
326
327     wrote_opt = False
328
329     for (suggest, allnames) in values.iteritems():
330       longnames = [i for i in allnames if i.startswith("--")]
331
332       if wrote_opt:
333         condcmd = "elif"
334       else:
335         condcmd = "if"
336
337       sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
338                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
339                utils.ShellQuote("|".join(allnames)))
340       sw.IncIndent()
341       try:
342         if suggest == cli.OPT_COMPL_MANY_NODES:
343           # TODO: Implement comma-separated values
344           WriteCompReply(sw, "-W ''", cur=cur)
345         elif suggest == cli.OPT_COMPL_ONE_NODE:
346           WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
347         elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
348           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
349         elif suggest == cli.OPT_COMPL_ONE_OS:
350           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
351         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
352           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
353         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
354           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
355
356           sw.Write("if [[ \"$optcur\" == *:* ]]; then")
357           sw.IncIndent()
358           try:
359             sw.Write("node1=\"${optcur%%:*}\"")
360
361             sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
362             sw.IncIndent()
363             try:
364               sw.Write("pfx=\"$node1:\"")
365             finally:
366               sw.DecIndent()
367             sw.Write("fi")
368           finally:
369             sw.DecIndent()
370           sw.Write("fi")
371
372           sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
373                    " node1=\"'$node1'\"")
374
375           sw.Write("for i in $(_ganeti_nodes); do")
376           sw.IncIndent()
377           try:
378             sw.Write("if [[ -z \"$node1\" ]]; then")
379             sw.IncIndent()
380             try:
381               sw.Write("tmp=\"$tmp $i $i:\"")
382             finally:
383               sw.DecIndent()
384             sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
385             sw.IncIndent()
386             try:
387               sw.Write("tmp=\"$tmp $i\"")
388             finally:
389               sw.DecIndent()
390             sw.Write("fi")
391           finally:
392             sw.DecIndent()
393           sw.Write("done")
394
395           WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
396         else:
397           WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
398       finally:
399         sw.DecIndent()
400
401       wrote_opt = True
402
403     if wrote_opt:
404       sw.Write("fi")
405
406     return
407
408   def _CompleteArguments(self, sw):
409     if not (self.opts or self.args):
410       return
411
412     all_option_names = []
413     for opt in self.opts:
414       all_option_names.extend(opt.all_names)
415     all_option_names.sort()
416
417     # List options if no argument has been specified yet
418     sw.Write("_ganeti_list_options %s",
419              utils.ShellQuote(" ".join(all_option_names)))
420
421     if self.args:
422       last_idx = len(self.args) - 1
423       last_arg_end = 0
424       varlen_arg_idx = None
425       wrote_arg = False
426
427       # Write some debug comments
428       for idx, arg in enumerate(self.args):
429         sw.Write("# %s: %r", idx, arg)
430
431       sw.Write("compgenargs=")
432
433       for idx, arg in enumerate(self.args):
434         assert arg.min is not None and arg.min >= 0
435         assert not (idx < last_idx and arg.max is None)
436
437         if arg.min != arg.max or arg.max is None:
438           if varlen_arg_idx is not None:
439             raise Exception("Only one argument can have a variable length")
440           varlen_arg_idx = idx
441
442         compgenargs = []
443
444         if isinstance(arg, cli.ArgUnknown):
445           choices = ""
446         elif isinstance(arg, cli.ArgSuggest):
447           choices = utils.ShellQuote(" ".join(arg.choices))
448         elif isinstance(arg, cli.ArgInstance):
449           choices = "$(_ganeti_instances)"
450         elif isinstance(arg, cli.ArgNode):
451           choices = "$(_ganeti_nodes)"
452         elif isinstance(arg, cli.ArgJobId):
453           choices = "$(_ganeti_jobs)"
454         elif isinstance(arg, cli.ArgFile):
455           choices = ""
456           compgenargs.append("-f")
457         elif isinstance(arg, cli.ArgCommand):
458           choices = ""
459           compgenargs.append("-c")
460         elif isinstance(arg, cli.ArgHost):
461           choices = ""
462           compgenargs.append("-A hostname")
463         else:
464           raise Exception("Unknown argument type %r" % arg)
465
466         if arg.min == 1 and arg.max == 1:
467           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
468         elif arg.max is None:
469           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
470         elif arg.min <= arg.max:
471           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
472                      (last_arg_end, last_arg_end + arg.max))
473         else:
474           raise Exception("Unable to generate argument position condition")
475
476         last_arg_end += arg.min
477
478         if choices or compgenargs:
479           if wrote_arg:
480             condcmd = "elif"
481           else:
482             condcmd = "if"
483
484           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
485           sw.IncIndent()
486           try:
487             if choices:
488               sw.Write("""choices="$choices "%s""", choices)
489             if compgenargs:
490               sw.Write("compgenargs=%s",
491                        utils.ShellQuote(" ".join(compgenargs)))
492           finally:
493             sw.DecIndent()
494
495           wrote_arg = True
496
497       if wrote_arg:
498         sw.Write("fi")
499
500     if self.args:
501       WriteCompReply(sw, """-W "$choices" $compgenargs""")
502     else:
503       # $compgenargs exists only if there are arguments
504       WriteCompReply(sw, '-W "$choices"')
505
506   def WriteTo(self, sw):
507     self._FindFirstArgument(sw)
508     self._CompleteOptionValues(sw)
509     self._CompleteArguments(sw)
510
511
512 def WriteCompletion(sw, scriptname, funcname,
513                     commands=None,
514                     opts=None, args=None):
515   """Writes the completion code for one command.
516
517   @type sw: ShellWriter
518   @param sw: Script writer
519   @type scriptname: string
520   @param scriptname: Name of command line program
521   @type funcname: string
522   @param funcname: Shell function name
523   @type commands: list
524   @param commands: List of all subcommands in this program
525
526   """
527   sw.Write("%s() {", funcname)
528   sw.IncIndent()
529   try:
530     sw.Write("local "
531              ' cur="${COMP_WORDS[COMP_CWORD]}"'
532              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
533              ' i first_arg_idx choices compgenargs arg_idx optcur')
534
535     sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
536     sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
537              " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
538
539     sw.Write("COMPREPLY=()")
540
541     if opts is not None and args is not None:
542       assert not commands
543       CompletionWriter(0, opts, args).WriteTo(sw)
544
545     else:
546       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
547       sw.IncIndent()
548       try:
549         # Complete the command name
550         WriteCompReply(sw,
551                        ("-W %s" %
552                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
553       finally:
554         sw.DecIndent()
555       sw.Write("fi")
556
557       # We're doing options and arguments to commands
558       sw.Write("""case "${COMP_WORDS[1]}" in""")
559       for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
560         if not (argdef or optdef):
561           continue
562
563         # TODO: Group by arguments and options
564         sw.Write("%s)", utils.ShellQuote(cmd))
565         sw.IncIndent()
566         try:
567           CompletionWriter(1, optdef, argdef).WriteTo(sw)
568         finally:
569           sw.DecIndent()
570
571         sw.Write(";;")
572       sw.Write("esac")
573   finally:
574     sw.DecIndent()
575   sw.Write("}")
576
577   sw.Write("complete -F %s -o filenames %s",
578            utils.ShellQuote(funcname),
579            utils.ShellQuote(scriptname))
580
581
582 def GetFunctionName(name):
583   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
584
585
586 def GetCommands(filename, module):
587   """Returns the commands defined in a module.
588
589   Aliases are also added as commands.
590
591   """
592   try:
593     commands = getattr(module, "commands")
594   except AttributeError, err:
595     raise Exception("Script %s doesn't have 'commands' attribute" %
596                     filename)
597
598   # Add the implicit "--help" option
599   help_option = cli.cli_option("-h", "--help", default=False,
600                                action="store_true")
601
602   for (_, _, optdef, _, _) in commands.itervalues():
603     if help_option not in optdef:
604       optdef.append(help_option)
605     if cli.DEBUG_OPT not in optdef:
606       optdef.append(cli.DEBUG_OPT)
607
608   # Use aliases
609   aliases = getattr(module, "aliases", {})
610   if aliases:
611     commands = commands.copy()
612     for name, target in aliases.iteritems():
613       commands[name] = commands[target]
614
615   return commands
616
617
618 def main():
619   buf = StringIO()
620   sw = ShellWriter(buf)
621
622   WritePreamble(sw)
623
624   # gnt-* scripts
625   for scriptname in _autoconf.GNT_SCRIPTS:
626     filename = "scripts/%s" % scriptname
627
628     WriteCompletion(sw, scriptname,
629                     GetFunctionName(scriptname),
630                     commands=GetCommands(filename,
631                                          build.LoadModule(filename)))
632
633   # Burnin script
634   burnin = build.LoadModule("tools/burnin")
635   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
636                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
637
638   print buf.getvalue()
639
640
641 if __name__ == "__main__":
642   main()