Bash completion: Simplify option completion
[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 imp
23 import optparse
24 import os
25 import sys
26 import re
27 from cStringIO import StringIO
28
29 from ganeti import constants
30 from ganeti import cli
31 from ganeti import utils
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_nodes() {")
85   sw.IncIndent()
86   try:
87     node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
88     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
89   finally:
90     sw.DecIndent()
91   sw.Write("}")
92
93   sw.Write("_ganeti_instances() {")
94   sw.IncIndent()
95   try:
96     instance_list_path = os.path.join(constants.DATA_DIR,
97                                       "ssconf_instance_list")
98     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
99   finally:
100     sw.DecIndent()
101   sw.Write("}")
102
103   sw.Write("_ganeti_jobs() {")
104   sw.IncIndent()
105   try:
106     # FIXME: this is really going into the internals of the job queue
107     sw.Write(("local jlist=$( shopt -s nullglob &&"
108               " cd %s 2>/dev/null && echo job-* || : )"),
109              utils.ShellQuote(constants.QUEUE_DIR))
110     sw.Write('echo "${jlist//job-/}"')
111   finally:
112     sw.DecIndent()
113   sw.Write("}")
114
115   sw.Write("_ganeti_os() {")
116   sw.IncIndent()
117   try:
118     # FIXME: Make querying the master for all OSes cheap
119     for path in constants.OS_SEARCH_PATH:
120       sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
121                utils.ShellQuote(path))
122   finally:
123     sw.DecIndent()
124   sw.Write("}")
125
126   # Params: <offset> <options with values> <options without values>
127   # Result variable: $first_arg_idx
128   sw.Write("_ganeti_find_first_arg() {")
129   sw.IncIndent()
130   try:
131     sw.Write("local w i")
132
133     sw.Write("first_arg_idx=")
134     sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
135     sw.IncIndent()
136     try:
137       sw.Write("w=${COMP_WORDS[$i]}")
138
139       # Skip option value
140       sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
141
142       # Skip
143       sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
144
145       # Ah, we found the first argument
146       sw.Write("else first_arg_idx=$i; break;")
147       sw.Write("fi")
148     finally:
149       sw.DecIndent()
150     sw.Write("done")
151   finally:
152     sw.DecIndent()
153   sw.Write("}")
154
155   # Params: <list of options separated by space>
156   # Input variable: $first_arg_idx
157   # Result variables: $arg_idx, $choices
158   sw.Write("_ganeti_list_options() {")
159   sw.IncIndent()
160   try:
161     sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
162     sw.IncIndent()
163     try:
164       sw.Write("arg_idx=0")
165       # Show options only if the current word starts with a dash
166       sw.Write("""if [[ "$cur" == -* ]]; then""")
167       sw.IncIndent()
168       try:
169         sw.Write("choices=$1")
170       finally:
171         sw.DecIndent()
172       sw.Write("fi")
173       sw.Write("return")
174     finally:
175       sw.DecIndent()
176     sw.Write("fi")
177
178     # Calculate position of current argument
179     sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
180     sw.Write("choices=")
181   finally:
182     sw.DecIndent()
183   sw.Write("}")
184
185   # Params: <long options with equal sign> <all options>
186   # Result variable: $optcur
187   sw.Write("_ganeti_checkopt() {")
188   sw.IncIndent()
189   try:
190     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
191     sw.IncIndent()
192     try:
193       sw.Write("optcur=\"${cur#--*=}\"")
194       sw.Write("return 0")
195     finally:
196       sw.DecIndent()
197     sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
198     sw.IncIndent()
199     try:
200       sw.Write("optcur=\"$cur\"")
201       sw.Write("return 0")
202     finally:
203       sw.DecIndent()
204     sw.Write("fi")
205
206     sw.Write("return 1")
207   finally:
208     sw.DecIndent()
209   sw.Write("}")
210
211
212 def WriteCompReply(sw, args, cur="\"$cur\""):
213   sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
214   sw.Write("return")
215
216
217 class CompletionWriter:
218   """Command completion writer class.
219
220   """
221   def __init__(self, arg_offset, opts, args):
222     self.arg_offset = arg_offset
223     self.opts = opts
224     self.args = args
225
226     for opt in opts:
227       opt.all_names = sorted(opt._short_opts + opt._long_opts)
228
229   def _FindFirstArgument(self, sw):
230     ignore = []
231     skip_one = []
232
233     for opt in self.opts:
234       if opt.takes_value():
235         # Ignore value
236         for i in opt.all_names:
237           if i.startswith("--"):
238             ignore.append("%s=*" % utils.ShellQuote(i))
239           skip_one.append(utils.ShellQuote(i))
240       else:
241         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
242
243     ignore = sorted(utils.UniqueSequence(ignore))
244     skip_one = sorted(utils.UniqueSequence(skip_one))
245
246     if ignore or skip_one:
247       # Try to locate first argument
248       sw.Write("_ganeti_find_first_arg %s %s %s",
249                self.arg_offset + 1,
250                utils.ShellQuote("|".join(skip_one)),
251                utils.ShellQuote("|".join(ignore)))
252     else:
253       # When there are no options the first argument is always at position
254       # offset + 1
255       sw.Write("first_arg_idx=%s", self.arg_offset + 1)
256
257   def _CompleteOptionValues(self, sw):
258     # Group by values
259     # "values" -> [optname1, optname2, ...]
260     values = {}
261
262     for opt in self.opts:
263       if not opt.takes_value():
264         continue
265
266       # Only static choices implemented so far (e.g. no node list)
267       suggest = getattr(opt, "completion_suggest", None)
268
269       if not suggest:
270         suggest = opt.choices
271
272       if suggest:
273         suggest_text = " ".join(sorted(suggest))
274       else:
275         suggest_text = ""
276
277       values.setdefault(suggest_text, []).extend(opt.all_names)
278
279     # Don't write any code if there are no option values
280     if not values:
281       return
282
283     cur = "\"$optcur\""
284
285     wrote_opt = False
286
287     for (suggest, allnames) in values.iteritems():
288       longnames = [i for i in allnames if i.startswith("--")]
289
290       if wrote_opt:
291         condcmd = "elif"
292       else:
293         condcmd = "if"
294
295       sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
296                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
297                utils.ShellQuote("|".join(allnames)))
298       sw.IncIndent()
299       try:
300         WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
301       finally:
302         sw.DecIndent()
303
304       wrote_opt = True
305
306     if wrote_opt:
307       sw.Write("fi")
308
309     return
310
311   def _CompleteArguments(self, sw):
312     if not (self.opts or self.args):
313       return
314
315     all_option_names = []
316     for opt in self.opts:
317       all_option_names.extend(opt.all_names)
318     all_option_names.sort()
319
320     # List options if no argument has been specified yet
321     sw.Write("_ganeti_list_options %s",
322              utils.ShellQuote(" ".join(all_option_names)))
323
324     if self.args:
325       last_idx = len(self.args) - 1
326       last_arg_end = 0
327       varlen_arg_idx = None
328       wrote_arg = False
329
330       # Write some debug comments
331       for idx, arg in enumerate(self.args):
332         sw.Write("# %s: %r", idx, arg)
333
334       sw.Write("compgenargs=")
335
336       for idx, arg in enumerate(self.args):
337         assert arg.min is not None and arg.min >= 0
338         assert not (idx < last_idx and arg.max is None)
339
340         if arg.min != arg.max or arg.max is None:
341           if varlen_arg_idx is not None:
342             raise Exception("Only one argument can have a variable length")
343           varlen_arg_idx = idx
344
345         compgenargs = []
346
347         if isinstance(arg, cli.ArgUnknown):
348           choices = ""
349         elif isinstance(arg, cli.ArgSuggest):
350           choices = utils.ShellQuote(" ".join(arg.choices))
351         elif isinstance(arg, cli.ArgInstance):
352           choices = "$(_ganeti_instances)"
353         elif isinstance(arg, cli.ArgNode):
354           choices = "$(_ganeti_nodes)"
355         elif isinstance(arg, cli.ArgJobId):
356           choices = "$(_ganeti_jobs)"
357         elif isinstance(arg, cli.ArgFile):
358           choices = ""
359           compgenargs.append("-f")
360         elif isinstance(arg, cli.ArgCommand):
361           choices = ""
362           compgenargs.append("-c")
363         elif isinstance(arg, cli.ArgHost):
364           choices = ""
365           compgenargs.append("-A hostname")
366         else:
367           raise Exception("Unknown argument type %r" % arg)
368
369         if arg.min == 1 and arg.max == 1:
370           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
371         elif arg.min <= arg.max:
372           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
373                      (last_arg_end, last_arg_end + arg.max))
374         elif arg.max is None:
375           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
376         else:
377           raise Exception("Unable to generate argument position condition")
378
379         last_arg_end += arg.min
380
381         if choices or compgenargs:
382           if wrote_arg:
383             condcmd = "elif"
384           else:
385             condcmd = "if"
386
387           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
388           sw.IncIndent()
389           try:
390             if choices:
391               sw.Write("""choices="$choices "%s""", choices)
392             if compgenargs:
393               sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
394           finally:
395             sw.DecIndent()
396
397           wrote_arg = True
398
399       if wrote_arg:
400         sw.Write("fi")
401
402     if self.args:
403       WriteCompReply(sw, """-W "$choices" $compgenargs""")
404     else:
405       # $compgenargs exists only if there are arguments
406       WriteCompReply(sw, '-W "$choices"')
407
408   def WriteTo(self, sw):
409     self._FindFirstArgument(sw)
410     self._CompleteOptionValues(sw)
411     self._CompleteArguments(sw)
412
413
414 def WriteCompletion(sw, scriptname, funcname,
415                     commands=None,
416                     opts=None, args=None):
417   """Writes the completion code for one command.
418
419   @type sw: ShellWriter
420   @param sw: Script writer
421   @type scriptname: string
422   @param scriptname: Name of command line program
423   @type funcname: string
424   @param funcname: Shell function name
425   @type commands: list
426   @param commands: List of all subcommands in this program
427
428   """
429   sw.Write("%s() {", funcname)
430   sw.IncIndent()
431   try:
432     sw.Write("local "
433              ' cur="${COMP_WORDS[COMP_CWORD]}"'
434              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
435              ' i first_arg_idx choices compgenargs arg_idx optcur')
436
437     # Useful for debugging:
438     #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
439     #sw.Write("set | grep ^COMP_")
440
441     sw.Write("COMPREPLY=()")
442
443     if opts is not None and args is not None:
444       assert not commands
445       CompletionWriter(0, opts, args).WriteTo(sw)
446
447     else:
448       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
449       sw.IncIndent()
450       try:
451         # Complete the command name
452         WriteCompReply(sw,
453                        ("-W %s" %
454                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
455       finally:
456         sw.DecIndent()
457       sw.Write("fi")
458
459       # We're doing options and arguments to commands
460       sw.Write("""case "${COMP_WORDS[1]}" in""")
461       for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
462         if not (argdef or optdef):
463           continue
464
465         # TODO: Group by arguments and options
466         sw.Write("%s)", utils.ShellQuote(cmd))
467         sw.IncIndent()
468         try:
469           CompletionWriter(1, optdef, argdef).WriteTo(sw)
470         finally:
471           sw.DecIndent()
472
473         sw.Write(";;")
474       sw.Write("esac")
475   finally:
476     sw.DecIndent()
477   sw.Write("}")
478
479   sw.Write("complete -F %s -o filenames %s",
480            utils.ShellQuote(funcname),
481            utils.ShellQuote(scriptname))
482
483
484 def GetFunctionName(name):
485   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
486
487
488 def LoadModule(filename):
489   """Loads an external module by filename.
490
491   """
492   (name, ext) = os.path.splitext(filename)
493
494   fh = open(filename, "U")
495   try:
496     return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
497   finally:
498     fh.close()
499
500
501 def GetCommands(filename, module):
502   """Returns the commands defined in a module.
503
504   Aliases are also added as commands.
505
506   """
507   try:
508     commands = getattr(module, "commands")
509   except AttributeError, err:
510     raise Exception("Script %s doesn't have 'commands' attribute" %
511                     filename)
512
513   # Use aliases
514   aliases = getattr(module, "aliases", {})
515   if aliases:
516     commands = commands.copy()
517     for name, target in aliases.iteritems():
518       commands[name] = commands[target]
519
520   return commands
521
522
523 def main():
524   buf = StringIO()
525   sw = ShellWriter(buf)
526
527   WritePreamble(sw)
528
529   # gnt-* scripts
530   for scriptname in _autoconf.GNT_SCRIPTS:
531     filename = "scripts/%s" % scriptname
532
533     WriteCompletion(sw, scriptname,
534                     GetFunctionName(scriptname),
535                     commands=GetCommands(filename, LoadModule(filename)))
536
537   # Burnin script
538   burnin = LoadModule("tools/burnin")
539   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
540                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
541
542   print buf.getvalue()
543
544
545 if __name__ == "__main__":
546   main()