Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 580ef58d

History | View | Annotate | Download (13.5 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
          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
      sw.Write("""case "$prev" in""")
261
      for (choices, names) in values.iteritems():
262
        # TODO: Implement completion for --foo=bar form
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
  def _CompleteArguments(self, sw):
276
    if not (self.opts or self.args):
277
      return
278

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

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

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

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

    
298
      sw.Write("compgenargs=")
299

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

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

    
309
        compgenargs = []
310

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

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

    
343
        last_arg_end += arg.min
344

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

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

    
361
          wrote_arg = True
362

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

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

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

    
377

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

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

    
392
  """
393
  sw.Write("%s() {", funcname)
394
  sw.IncIndent()
395
  try:
396
    sw.Write("local "
397
             ' cur="${COMP_WORDS[$COMP_CWORD]}"'
398
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
399
             ' i first_arg_idx choices compgenargs arg_idx')
400

    
401
    # Useful for debugging:
402
    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
403
    #sw.Write("set | grep ^COMP_")
404

    
405
    sw.Write("COMPREPLY=()")
406

    
407
    if opts is not None and args is not None:
408
      assert not commands
409
      CompletionWriter(0, opts, args).WriteTo(sw)
410

    
411
    else:
412
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
413
      sw.IncIndent()
414
      try:
415
        # Complete the command name
416
        WriteCompReply(sw,
417
                       ("-W %s" %
418
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
419
      finally:
420
        sw.DecIndent()
421
      sw.Write("fi")
422

    
423
      # We're doing options and arguments to commands
424
      sw.Write("""case "${COMP_WORDS[1]}" in""")
425
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
426
        if not (argdef or optdef):
427
          continue
428

    
429
        # TODO: Group by arguments and options
430
        sw.Write("%s)", utils.ShellQuote(cmd))
431
        sw.IncIndent()
432
        try:
433
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
434
        finally:
435
          sw.DecIndent()
436

    
437
        sw.Write(";;")
438
      sw.Write("esac")
439
  finally:
440
    sw.DecIndent()
441
  sw.Write("}")
442

    
443
  sw.Write("complete -F %s -o filenames %s",
444
           utils.ShellQuote(funcname),
445
           utils.ShellQuote(scriptname))
446

    
447

    
448
def GetFunctionName(name):
449
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
450

    
451

    
452
def LoadModule(filename):
453
  """Loads an external module by filename.
454

    
455
  """
456
  (name, ext) = os.path.splitext(filename)
457

    
458
  fh = open(filename, "U")
459
  try:
460
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
461
  finally:
462
    fh.close()
463

    
464

    
465
def GetCommands(filename, module):
466
  """Returns the commands defined in a module.
467

    
468
  Aliases are also added as commands.
469

    
470
  """
471
  try:
472
    commands = getattr(module, "commands")
473
  except AttributeError, err:
474
    raise Exception("Script %s doesn't have 'commands' attribute" %
475
                    filename)
476

    
477
  # Use aliases
478
  aliases = getattr(module, "aliases", {})
479
  if aliases:
480
    commands = commands.copy()
481
    for name, target in aliases.iteritems():
482
      commands[name] = commands[target]
483

    
484
  return commands
485

    
486

    
487
def main():
488
  buf = StringIO()
489
  sw = ShellWriter(buf)
490

    
491
  WritePreamble(sw)
492

    
493
  # gnt-* scripts
494
  for scriptname in _autoconf.GNT_SCRIPTS:
495
    filename = "scripts/%s" % scriptname
496

    
497
    WriteCompletion(sw, scriptname,
498
                    GetFunctionName(scriptname),
499
                    commands=GetCommands(filename, LoadModule(filename)))
500

    
501
  # Burnin script
502
  burnin = LoadModule("tools/burnin")
503
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
504
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
505

    
506
  print buf.getvalue()
507

    
508

    
509
if __name__ == "__main__":
510
  main()