Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 5a78e2e7

History | View | Annotate | Download (13.3 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 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):
187
  sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args)
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
          ignore.append("%s=*" % utils.ShellQuote(i))
212
          skip_one.append(utils.ShellQuote(i))
213
      else:
214
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
215

    
216
    ignore = sorted(utils.UniqueSequence(ignore))
217
    skip_one = sorted(utils.UniqueSequence(skip_one))
218

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

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

    
235
    for opt in self.opts:
236
      if not opt.takes_value():
237
        continue
238

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

    
242
      if not suggest:
243
        suggest = opt.choices
244

    
245
      if suggest:
246
        suggest_text = " ".join(sorted(suggest))
247
      else:
248
        suggest_text = ""
249

    
250
      values.setdefault(suggest_text, []).extend(opt.all_names)
251

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

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

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

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

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

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

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

    
297
      sw.Write("compgenargs=")
298

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

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

    
308
        compgenargs = []
309

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

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

    
342
        last_arg_end += arg.min
343

    
344
        if choices or compgenargs:
345
          if wrote_arg:
346
            condcmd = "elif"
347
          else:
348
            condcmd = "if"
349

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

    
360
          wrote_arg = True
361

    
362
      if wrote_arg:
363
        sw.Write("fi")
364

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

    
371
  def WriteTo(self, sw):
372
    self._FindFirstArgument(sw)
373
    self._CompleteOptionValues(sw)
374
    self._CompleteArguments(sw)
375

    
376

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

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

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

    
398
    sw.Write("COMPREPLY=()")
399

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

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

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

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

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

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

    
440

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

    
444

    
445
def LoadModule(filename):
446
  """Loads an external module by filename.
447

    
448
  """
449
  (name, ext) = os.path.splitext(filename)
450

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

    
457

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

    
461
  Aliases are also added as commands.
462

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

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

    
477
  return commands
478

    
479

    
480
def main():
481
  buf = StringIO()
482
  sw = ShellWriter(buf)
483

    
484
  WritePreamble(sw)
485

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

    
490
    WriteCompletion(sw, scriptname,
491
                    GetFunctionName(scriptname),
492
                    commands=GetCommands(filename, LoadModule(filename)))
493

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

    
499
  print buf.getvalue()
500

    
501

    
502
if __name__ == "__main__":
503
  main()