Bash completion: Support for --foo=bar option format
[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
186 def WriteCompReply(sw, args, cur="\"$cur\""):
187   sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
188   sw.Write("return")
189
190
191 class CompletionWriter:
192   """Command completion writer class.
193
194   """
195   def __init__(self, arg_offset, opts, args):
196     self.arg_offset = arg_offset
197     self.opts = opts
198     self.args = args
199
200     for opt in opts:
201       opt.all_names = sorted(opt._short_opts + opt._long_opts)
202
203   def _FindFirstArgument(self, sw):
204     ignore = []
205     skip_one = []
206
207     for opt in self.opts:
208       if opt.takes_value():
209         # Ignore value
210         for i in opt.all_names:
211           if i.startswith("--"):
212             ignore.append("%s=*" % utils.ShellQuote(i))
213           skip_one.append(utils.ShellQuote(i))
214       else:
215         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
216
217     ignore = sorted(utils.UniqueSequence(ignore))
218     skip_one = sorted(utils.UniqueSequence(skip_one))
219
220     if ignore or skip_one:
221       # Try to locate first argument
222       sw.Write("_ganeti_find_first_arg %s %s %s",
223                self.arg_offset + 1,
224                utils.ShellQuote("|".join(skip_one)),
225                utils.ShellQuote("|".join(ignore)))
226     else:
227       # When there are no options the first argument is always at position
228       # offset + 1
229       sw.Write("first_arg_idx=%s", self.arg_offset + 1)
230
231   def _CompleteOptionValues(self, sw):
232     # Group by values
233     # "values" -> [optname1, optname2, ...]
234     values = {}
235
236     for opt in self.opts:
237       if not opt.takes_value():
238         continue
239
240       # Only static choices implemented so far (e.g. no node list)
241       suggest = getattr(opt, "completion_suggest", None)
242
243       if not suggest:
244         suggest = opt.choices
245
246       if suggest:
247         suggest_text = " ".join(sorted(suggest))
248       else:
249         suggest_text = ""
250
251       values.setdefault(suggest_text, []).extend(opt.all_names)
252
253     # Don't write any code if there are no option values
254     if not values:
255       return
256
257     sw.Write("if [[ $COMP_CWORD -gt %s ]]; then", self.arg_offset + 1)
258     sw.IncIndent()
259     try:
260       # --foo value
261       sw.Write("""case "$prev" in""")
262       for (choices, names) in values.iteritems():
263         sw.Write("%s)", "|".join([utils.ShellQuote(i) for i in names]))
264         sw.IncIndent()
265         try:
266           WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices))
267         finally:
268           sw.DecIndent()
269         sw.Write(";;")
270       sw.Write("""esac""")
271     finally:
272       sw.DecIndent()
273     sw.Write("""fi""")
274
275     # --foo=value
276     values_longopts = {}
277
278     for (choices, names) in values.iteritems():
279       longnames = [i for i in names if i.startswith("--")]
280       if longnames:
281         values_longopts[choices] = longnames
282
283     if values_longopts:
284       sw.Write("""case "$cur" in""")
285       for (choices, names) in values_longopts.iteritems():
286         sw.Write("%s)", "|".join([utils.ShellQuote(i) + "=*" for i in names]))
287         sw.IncIndent()
288         try:
289           # Shell expression to get option value
290           cur="\"${cur#--*=}\""
291           WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices), cur=cur)
292         finally:
293           sw.DecIndent()
294         sw.Write(";;")
295       sw.Write("""esac""")
296
297   def _CompleteArguments(self, sw):
298     if not (self.opts or self.args):
299       return
300
301     all_option_names = []
302     for opt in self.opts:
303       all_option_names.extend(opt.all_names)
304     all_option_names.sort()
305
306     # List options if no argument has been specified yet
307     sw.Write("_ganeti_list_options %s",
308              utils.ShellQuote(" ".join(all_option_names)))
309
310     if self.args:
311       last_idx = len(self.args) - 1
312       last_arg_end = 0
313       varlen_arg_idx = None
314       wrote_arg = False
315
316       # Write some debug comments
317       for idx, arg in enumerate(self.args):
318         sw.Write("# %s: %r", idx, arg)
319
320       sw.Write("compgenargs=")
321
322       for idx, arg in enumerate(self.args):
323         assert arg.min is not None and arg.min >= 0
324         assert not (idx < last_idx and arg.max is None)
325
326         if arg.min != arg.max or arg.max is None:
327           if varlen_arg_idx is not None:
328             raise Exception("Only one argument can have a variable length")
329           varlen_arg_idx = idx
330
331         compgenargs = []
332
333         if isinstance(arg, cli.ArgUnknown):
334           choices = ""
335         elif isinstance(arg, cli.ArgSuggest):
336           choices = utils.ShellQuote(" ".join(arg.choices))
337         elif isinstance(arg, cli.ArgInstance):
338           choices = "$(_ganeti_instances)"
339         elif isinstance(arg, cli.ArgNode):
340           choices = "$(_ganeti_nodes)"
341         elif isinstance(arg, cli.ArgJobId):
342           choices = "$(_ganeti_jobs)"
343         elif isinstance(arg, cli.ArgFile):
344           choices = ""
345           compgenargs.append("-f")
346         elif isinstance(arg, cli.ArgCommand):
347           choices = ""
348           compgenargs.append("-c")
349         elif isinstance(arg, cli.ArgHost):
350           choices = ""
351           compgenargs.append("-A hostname")
352         else:
353           raise Exception("Unknown argument type %r" % arg)
354
355         if arg.min == 1 and arg.max == 1:
356           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
357         elif arg.min <= arg.max:
358           cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
359                      (last_arg_end, last_arg_end + arg.max))
360         elif arg.max is None:
361           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
362         else:
363           raise Exception("Unable to generate argument position condition")
364
365         last_arg_end += arg.min
366
367         if choices or compgenargs:
368           if wrote_arg:
369             condcmd = "elif"
370           else:
371             condcmd = "if"
372
373           sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
374           sw.IncIndent()
375           try:
376             if choices:
377               sw.Write("""choices="$choices "%s""", choices)
378             if compgenargs:
379               sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
380           finally:
381             sw.DecIndent()
382
383           wrote_arg = True
384
385       if wrote_arg:
386         sw.Write("fi")
387
388     if self.args:
389       WriteCompReply(sw, """-W "$choices" $compgenargs""")
390     else:
391       # $compgenargs exists only if there are arguments
392       WriteCompReply(sw, '-W "$choices"')
393
394   def WriteTo(self, sw):
395     self._FindFirstArgument(sw)
396     self._CompleteOptionValues(sw)
397     self._CompleteArguments(sw)
398
399
400 def WriteCompletion(sw, scriptname, funcname,
401                     commands=None,
402                     opts=None, args=None):
403   """Writes the completion code for one command.
404
405   @type sw: ShellWriter
406   @param sw: Script writer
407   @type scriptname: string
408   @param scriptname: Name of command line program
409   @type funcname: string
410   @param funcname: Shell function name
411   @type commands: list
412   @param commands: List of all subcommands in this program
413
414   """
415   sw.Write("%s() {", funcname)
416   sw.IncIndent()
417   try:
418     sw.Write("local "
419              ' cur="${COMP_WORDS[COMP_CWORD]}"'
420              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
421              ' i first_arg_idx choices compgenargs arg_idx')
422
423     # Useful for debugging:
424     #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
425     #sw.Write("set | grep ^COMP_")
426
427     sw.Write("COMPREPLY=()")
428
429     if opts is not None and args is not None:
430       assert not commands
431       CompletionWriter(0, opts, args).WriteTo(sw)
432
433     else:
434       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
435       sw.IncIndent()
436       try:
437         # Complete the command name
438         WriteCompReply(sw,
439                        ("-W %s" %
440                         utils.ShellQuote(" ".join(sorted(commands.keys())))))
441       finally:
442         sw.DecIndent()
443       sw.Write("fi")
444
445       # We're doing options and arguments to commands
446       sw.Write("""case "${COMP_WORDS[1]}" in""")
447       for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
448         if not (argdef or optdef):
449           continue
450
451         # TODO: Group by arguments and options
452         sw.Write("%s)", utils.ShellQuote(cmd))
453         sw.IncIndent()
454         try:
455           CompletionWriter(1, optdef, argdef).WriteTo(sw)
456         finally:
457           sw.DecIndent()
458
459         sw.Write(";;")
460       sw.Write("esac")
461   finally:
462     sw.DecIndent()
463   sw.Write("}")
464
465   sw.Write("complete -F %s -o filenames %s",
466            utils.ShellQuote(funcname),
467            utils.ShellQuote(scriptname))
468
469
470 def GetFunctionName(name):
471   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
472
473
474 def LoadModule(filename):
475   """Loads an external module by filename.
476
477   """
478   (name, ext) = os.path.splitext(filename)
479
480   fh = open(filename, "U")
481   try:
482     return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
483   finally:
484     fh.close()
485
486
487 def GetCommands(filename, module):
488   """Returns the commands defined in a module.
489
490   Aliases are also added as commands.
491
492   """
493   try:
494     commands = getattr(module, "commands")
495   except AttributeError, err:
496     raise Exception("Script %s doesn't have 'commands' attribute" %
497                     filename)
498
499   # Use aliases
500   aliases = getattr(module, "aliases", {})
501   if aliases:
502     commands = commands.copy()
503     for name, target in aliases.iteritems():
504       commands[name] = commands[target]
505
506   return commands
507
508
509 def main():
510   buf = StringIO()
511   sw = ShellWriter(buf)
512
513   WritePreamble(sw)
514
515   # gnt-* scripts
516   for scriptname in _autoconf.GNT_SCRIPTS:
517     filename = "scripts/%s" % scriptname
518
519     WriteCompletion(sw, scriptname,
520                     GetFunctionName(scriptname),
521                     commands=GetCommands(filename, LoadModule(filename)))
522
523   # Burnin script
524   burnin = LoadModule("tools/burnin")
525   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
526                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
527
528   print buf.getvalue()
529
530
531 if __name__ == "__main__":
532   main()