Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 83ec7961

History | View | Annotate | Download (13.2 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
        elif isinstance(arg, cli.ArgHost):
325
          choices = ""
326
          compgenargs.append("-A hostname")
327
        else:
328
          raise Exception("Unknown argument type %r" % arg)
329

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

    
340
        last_arg_end += arg.min
341

    
342
        if choices or compgenargs:
343
          if wrote_arg:
344
            condcmd = "elif"
345
          else:
346
            condcmd = "if"
347

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

    
358
          wrote_arg = True
359

    
360
      if wrote_arg:
361
        sw.Write("fi")
362

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

    
369
  def WriteTo(self, sw):
370
    self._FindFirstArgument(sw)
371
    self._CompleteOptionValues(sw)
372
    self._CompleteArguments(sw)
373

    
374

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

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

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

    
396
    sw.Write("COMPREPLY=()")
397

    
398
    if opts is not None and args is not None:
399
      assert not commands
400
      CompletionWriter(0, opts, args).WriteTo(sw)
401

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

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

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

    
428
        sw.Write(";;")
429
      sw.Write("esac")
430
  finally:
431
    sw.DecIndent()
432
  sw.Write("}")
433

    
434
  sw.Write("complete -F %s -o filenames %s",
435
           utils.ShellQuote(funcname),
436
           utils.ShellQuote(scriptname))
437

    
438

    
439
def GetFunctionName(name):
440
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
441

    
442

    
443
def LoadModule(filename):
444
  """Loads an external module by filename.
445

    
446
  """
447
  (name, ext) = os.path.splitext(filename)
448

    
449
  fh = open(filename, "U")
450
  try:
451
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
452
  finally:
453
    fh.close()
454

    
455

    
456
def GetCommands(filename, module):
457
  """Returns the commands defined in a module.
458

    
459
  Aliases are also added as commands.
460

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

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

    
475
  return commands
476

    
477

    
478
def main():
479
  buf = StringIO()
480
  sw = ShellWriter(buf)
481

    
482
  WritePreamble(sw)
483

    
484
  # gnt-* scripts
485
  for scriptname in _autoconf.GNT_SCRIPTS:
486
    filename = "scripts/%s" % scriptname
487

    
488
    WriteCompletion(sw, scriptname,
489
                    GetFunctionName(scriptname),
490
                    commands=GetCommands(filename, LoadModule(filename)))
491

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

    
497
  print buf.getvalue()
498

    
499

    
500
if __name__ == "__main__":
501
  main()