Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 4f3d5b76

History | View | Annotate | Download (13.1 kB)

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", 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", 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=$( cd %s && echo job-*; )",
108
             utils.ShellQuote(constants.QUEUE_DIR))
109
    sw.Write("echo ${jlist//job-/}")
110
  finally:
111
    sw.DecIndent()
112
  sw.Write("}")
113

    
114
  sw.Write("_ganeti_os() {")
115
  sw.IncIndent()
116
  try:
117
    # FIXME: Make querying the master for all OSes cheap
118
    for path in constants.OS_SEARCH_PATH:
119
      sw.Write("( cd %s && echo *; )", utils.ShellQuote(path))
120
  finally:
121
    sw.DecIndent()
122
  sw.Write("}")
123

    
124
  # Params: <offset> <options with values> <options without values>
125
  # Result variable: $first_arg_idx
126
  sw.Write("_ganeti_find_first_arg() {")
127
  sw.IncIndent()
128
  try:
129
    sw.Write("local w i")
130

    
131
    sw.Write("first_arg_idx=")
132
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
133
    sw.IncIndent()
134
    try:
135
      sw.Write("w=${COMP_WORDS[$i]}")
136

    
137
      # Skip option value
138
      sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
139

    
140
      # Skip
141
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
142

    
143
      # Ah, we found the first argument
144
      sw.Write("else first_arg_idx=$i; break;")
145
      sw.Write("fi")
146
    finally:
147
      sw.DecIndent()
148
    sw.Write("done")
149
  finally:
150
    sw.DecIndent()
151
  sw.Write("}")
152

    
153
  # Params: <list of options separated by space>
154
  # Input variable: $first_arg_idx
155
  # Result variables: $arg_idx, $choices
156
  sw.Write("_ganeti_list_options() {")
157
  sw.IncIndent()
158
  try:
159
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
160
    sw.IncIndent()
161
    try:
162
      sw.Write("arg_idx=0")
163
      # Show options only if the current word starts with a dash
164
      sw.Write("""if [[ "$cur" == -* ]]; then""")
165
      sw.IncIndent()
166
      try:
167
        sw.Write("choices=$1")
168
      finally:
169
        sw.DecIndent()
170
      sw.Write("fi")
171
      sw.Write("return")
172
    finally:
173
      sw.DecIndent()
174
    sw.Write("fi")
175

    
176
    # Calculate position of current argument
177
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
178
    sw.Write("choices=")
179
  finally:
180
    sw.DecIndent()
181
  sw.Write("}")
182

    
183

    
184
def WriteCompReply(sw, args):
185
  sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args)
186
  sw.Write("return")
187

    
188

    
189
class CompletionWriter:
190
  """Command completion writer class.
191

    
192
  """
193
  def __init__(self, arg_offset, opts, args):
194
    self.arg_offset = arg_offset
195
    self.opts = opts
196
    self.args = args
197

    
198
    for opt in opts:
199
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
200

    
201
  def _FindFirstArgument(self, sw):
202
    ignore = []
203
    skip_one = []
204

    
205
    for opt in self.opts:
206
      if opt.takes_value():
207
        # Ignore value
208
        for i in opt.all_names:
209
          ignore.append("%s=*" % utils.ShellQuote(i))
210
          skip_one.append(utils.ShellQuote(i))
211
      else:
212
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
213

    
214
    ignore = sorted(utils.UniqueSequence(ignore))
215
    skip_one = sorted(utils.UniqueSequence(skip_one))
216

    
217
    if ignore or skip_one:
218
      # Try to locate first argument
219
      sw.Write("_ganeti_find_first_arg %s %s %s",
220
               self.arg_offset + 1,
221
               utils.ShellQuote("|".join(skip_one)),
222
               utils.ShellQuote("|".join(ignore)))
223
    else:
224
      # When there are no options the first argument is always at position
225
      # offset + 1
226
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
227

    
228
  def _CompleteOptionValues(self, sw):
229
    # Group by values
230
    # "values" -> [optname1, optname2, ...]
231
    values = {}
232

    
233
    for opt in self.opts:
234
      if not opt.takes_value():
235
        continue
236

    
237
      # Only static choices implemented so far (e.g. no node list)
238
      suggest = getattr(opt, "completion_suggest", None)
239

    
240
      if not suggest:
241
        suggest = opt.choices
242

    
243
      if suggest:
244
        suggest_text = " ".join(sorted(suggest))
245
      else:
246
        suggest_text = ""
247

    
248
      values.setdefault(suggest_text, []).extend(opt.all_names)
249

    
250
    # Don't write any code if there are no option values
251
    if not values:
252
      return
253

    
254
    sw.Write("if [[ $COMP_CWORD -gt %s ]]; then", self.arg_offset + 1)
255
    sw.IncIndent()
256
    try:
257
      sw.Write("""case "$prev" in""")
258
      for (choices, names) in values.iteritems():
259
        # TODO: Implement completion for --foo=bar form
260
        sw.Write("%s)", "|".join([utils.ShellQuote(i) for i in names]))
261
        sw.IncIndent()
262
        try:
263
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices))
264
        finally:
265
          sw.DecIndent()
266
        sw.Write(";;")
267
      sw.Write("""esac""")
268
    finally:
269
      sw.DecIndent()
270
    sw.Write("""fi""")
271

    
272
  def _CompleteArguments(self, sw):
273
    if not (self.opts or self.args):
274
      return
275

    
276
    all_option_names = []
277
    for opt in self.opts:
278
      all_option_names.extend(opt.all_names)
279
    all_option_names.sort()
280

    
281
    # List options if no argument has been specified yet
282
    sw.Write("_ganeti_list_options %s",
283
             utils.ShellQuote(" ".join(all_option_names)))
284

    
285
    if self.args:
286
      last_idx = len(self.args) - 1
287
      last_arg_end = 0
288
      varlen_arg_idx = None
289
      wrote_arg = False
290

    
291
      # Write some debug comments
292
      for idx, arg in enumerate(self.args):
293
        sw.Write("# %s: %r", idx, arg)
294

    
295
      sw.Write("compgenargs=")
296

    
297
      for idx, arg in enumerate(self.args):
298
        assert arg.min is not None and arg.min >= 0
299
        assert not (idx < last_idx and arg.max is None)
300

    
301
        if arg.min != arg.max or arg.max is None:
302
          if varlen_arg_idx is not None:
303
            raise Exception("Only one argument can have a variable length")
304
          varlen_arg_idx = idx
305

    
306
        compgenargs = []
307

    
308
        if isinstance(arg, cli.ArgUnknown):
309
          choices = ""
310
        elif isinstance(arg, cli.ArgSuggest):
311
          choices = utils.ShellQuote(" ".join(arg.choices))
312
        elif isinstance(arg, cli.ArgInstance):
313
          choices = "$(_ganeti_instances)"
314
        elif isinstance(arg, cli.ArgNode):
315
          choices = "$(_ganeti_nodes)"
316
        elif isinstance(arg, cli.ArgJobId):
317
          choices = "$(_ganeti_jobs)"
318
        elif isinstance(arg, cli.ArgFile):
319
          choices = ""
320
          compgenargs.append("-f")
321
        elif isinstance(arg, cli.ArgCommand):
322
          choices = ""
323
          compgenargs.append("-c")
324
        else:
325
          raise Exception("Unknown argument type %r" % arg)
326

    
327
        if arg.min == 1 and arg.max == 1:
328
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
329
        elif arg.min == arg.max:
330
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
331
                     (last_arg_end, last_arg_end + arg.max))
332
        elif arg.max is None:
333
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
334
        else:
335
          raise Exception("Unable to generate argument position condition")
336

    
337
        last_arg_end += arg.min
338

    
339
        if choices or compgenargs:
340
          if wrote_arg:
341
            condcmd = "elif"
342
          else:
343
            condcmd = "if"
344

    
345
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
346
          sw.IncIndent()
347
          try:
348
            if choices:
349
              sw.Write("""choices="$choices "%s""", choices)
350
            if compgenargs:
351
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
352
          finally:
353
            sw.DecIndent()
354

    
355
          wrote_arg = True
356

    
357
      if wrote_arg:
358
        sw.Write("fi")
359

    
360
    if self.args:
361
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
362
    else:
363
      # $compgenargs exists only if there are arguments
364
      WriteCompReply(sw, '-W "$choices"')
365

    
366
  def WriteTo(self, sw):
367
    self._FindFirstArgument(sw)
368
    self._CompleteOptionValues(sw)
369
    self._CompleteArguments(sw)
370

    
371

    
372
def WriteCompletion(sw, scriptname, funcname,
373
                    commands=None,
374
                    opts=None, args=None):
375
  """Writes the completion code for one command.
376

    
377
  @type sw: ShellWriter
378
  @param sw: Script writer
379
  @type scriptname: string
380
  @param scriptname: Name of command line program
381
  @type funcname: string
382
  @param funcname: Shell function name
383
  @type commands: list
384
  @param commands: List of all subcommands in this program
385

    
386
  """
387
  sw.Write("%s() {", funcname)
388
  sw.IncIndent()
389
  try:
390
    sw.Write('local cur="$2" prev="$3"')
391
    sw.Write("local i first_arg_idx choices compgenargs arg_idx")
392

    
393
    sw.Write("COMPREPLY=()")
394

    
395
    if opts is not None and args is not None:
396
      assert not commands
397
      CompletionWriter(0, opts, args).WriteTo(sw)
398

    
399
    else:
400
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
401
      sw.IncIndent()
402
      try:
403
        # Complete the command name
404
        WriteCompReply(sw,
405
                       ("-W %s" %
406
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
407
      finally:
408
        sw.DecIndent()
409
      sw.Write("fi")
410

    
411
      # We're doing options and arguments to commands
412
      sw.Write("""case "${COMP_WORDS[1]}" in""")
413
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
414
        if not (argdef or optdef):
415
          continue
416

    
417
        # TODO: Group by arguments and options
418
        sw.Write("%s)", utils.ShellQuote(cmd))
419
        sw.IncIndent()
420
        try:
421
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
422
        finally:
423
          sw.DecIndent()
424

    
425
        sw.Write(";;")
426
      sw.Write("esac")
427
  finally:
428
    sw.DecIndent()
429
  sw.Write("}")
430

    
431
  sw.Write("complete -F %s -o filenames %s",
432
           utils.ShellQuote(funcname),
433
           utils.ShellQuote(scriptname))
434

    
435

    
436
def GetFunctionName(name):
437
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
438

    
439

    
440
def LoadModule(filename):
441
  """Loads an external module by filename.
442

    
443
  """
444
  (name, ext) = os.path.splitext(filename)
445

    
446
  fh = open(filename, "U")
447
  try:
448
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
449
  finally:
450
    fh.close()
451

    
452

    
453
def GetCommands(filename, module):
454
  """Returns the commands defined in a module.
455

    
456
  Aliases are also added as commands.
457

    
458
  """
459
  try:
460
    commands = getattr(module, "commands")
461
  except AttributeError, err:
462
    raise Exception("Script %s doesn't have 'commands' attribute" %
463
                    filename)
464

    
465
  # Use aliases
466
  aliases = getattr(module, "aliases", {})
467
  if aliases:
468
    commands = commands.copy()
469
    for name, target in aliases.iteritems():
470
      commands[name] = commands[target]
471

    
472
  return commands
473

    
474

    
475
def main():
476
  buf = StringIO()
477
  sw = ShellWriter(buf)
478

    
479
  WritePreamble(sw)
480

    
481
  # gnt-* scripts
482
  for scriptname in _autoconf.GNT_SCRIPTS:
483
    filename = "scripts/%s" % scriptname
484

    
485
    WriteCompletion(sw, scriptname,
486
                    GetFunctionName(scriptname),
487
                    commands=GetCommands(filename, LoadModule(filename)))
488

    
489
  # Burnin script
490
  burnin = LoadModule("tools/burnin")
491
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
492
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
493

    
494
  print buf.getvalue()
495

    
496

    
497
if __name__ == "__main__":
498
  main()