Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 30d44392

History | View | Annotate | Download (14.4 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
  for (fnname, paths) in [
116
      ("os", constants.OS_SEARCH_PATH),
117
      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
118
      ]:
119
    sw.Write("_ganeti_%s() {", fnname)
120
    sw.IncIndent()
121
    try:
122
      # FIXME: Make querying the master for all OSes cheap
123
      for path in paths:
124
        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
125
                 utils.ShellQuote(path))
126
    finally:
127
      sw.DecIndent()
128
    sw.Write("}")
129

    
130
  # Params: <offset> <options with values> <options without values>
131
  # Result variable: $first_arg_idx
132
  sw.Write("_ganeti_find_first_arg() {")
133
  sw.IncIndent()
134
  try:
135
    sw.Write("local w i")
136

    
137
    sw.Write("first_arg_idx=")
138
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
139
    sw.IncIndent()
140
    try:
141
      sw.Write("w=${COMP_WORDS[$i]}")
142

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

    
146
      # Skip
147
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
148

    
149
      # Ah, we found the first argument
150
      sw.Write("else first_arg_idx=$i; break;")
151
      sw.Write("fi")
152
    finally:
153
      sw.DecIndent()
154
    sw.Write("done")
155
  finally:
156
    sw.DecIndent()
157
  sw.Write("}")
158

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

    
182
    # Calculate position of current argument
183
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
184
    sw.Write("choices=")
185
  finally:
186
    sw.DecIndent()
187
  sw.Write("}")
188

    
189
  # Params: <long options with equal sign> <all options>
190
  # Result variable: $optcur
191
  sw.Write("_ganeti_checkopt() {")
192
  sw.IncIndent()
193
  try:
194
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
195
    sw.IncIndent()
196
    try:
197
      sw.Write("optcur=\"${cur#--*=}\"")
198
      sw.Write("return 0")
199
    finally:
200
      sw.DecIndent()
201
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
202
    sw.IncIndent()
203
    try:
204
      sw.Write("optcur=\"$cur\"")
205
      sw.Write("return 0")
206
    finally:
207
      sw.DecIndent()
208
    sw.Write("fi")
209

    
210
    sw.Write("return 1")
211
  finally:
212
    sw.DecIndent()
213
  sw.Write("}")
214

    
215

    
216
def WriteCompReply(sw, args, cur="\"$cur\""):
217
  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
218
  sw.Write("return")
219

    
220

    
221
class CompletionWriter:
222
  """Command completion writer class.
223

    
224
  """
225
  def __init__(self, arg_offset, opts, args):
226
    self.arg_offset = arg_offset
227
    self.opts = opts
228
    self.args = args
229

    
230
    for opt in opts:
231
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
232

    
233
  def _FindFirstArgument(self, sw):
234
    ignore = []
235
    skip_one = []
236

    
237
    for opt in self.opts:
238
      if opt.takes_value():
239
        # Ignore value
240
        for i in opt.all_names:
241
          if i.startswith("--"):
242
            ignore.append("%s=*" % utils.ShellQuote(i))
243
          skip_one.append(utils.ShellQuote(i))
244
      else:
245
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
246

    
247
    ignore = sorted(utils.UniqueSequence(ignore))
248
    skip_one = sorted(utils.UniqueSequence(skip_one))
249

    
250
    if ignore or skip_one:
251
      # Try to locate first argument
252
      sw.Write("_ganeti_find_first_arg %s %s %s",
253
               self.arg_offset + 1,
254
               utils.ShellQuote("|".join(skip_one)),
255
               utils.ShellQuote("|".join(ignore)))
256
    else:
257
      # When there are no options the first argument is always at position
258
      # offset + 1
259
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
260

    
261
  def _CompleteOptionValues(self, sw):
262
    # Group by values
263
    # "values" -> [optname1, optname2, ...]
264
    values = {}
265

    
266
    for opt in self.opts:
267
      if not opt.takes_value():
268
        continue
269

    
270
      # Only static choices implemented so far (e.g. no node list)
271
      suggest = getattr(opt, "completion_suggest", None)
272

    
273
      if not suggest:
274
        suggest = opt.choices
275

    
276
      if suggest:
277
        suggest_text = " ".join(sorted(suggest))
278
      else:
279
        suggest_text = ""
280

    
281
      values.setdefault(suggest_text, []).extend(opt.all_names)
282

    
283
    # Don't write any code if there are no option values
284
    if not values:
285
      return
286

    
287
    cur = "\"$optcur\""
288

    
289
    wrote_opt = False
290

    
291
    for (suggest, allnames) in values.iteritems():
292
      longnames = [i for i in allnames if i.startswith("--")]
293

    
294
      if wrote_opt:
295
        condcmd = "elif"
296
      else:
297
        condcmd = "if"
298

    
299
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
300
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
301
               utils.ShellQuote("|".join(allnames)))
302
      sw.IncIndent()
303
      try:
304
        WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
305
      finally:
306
        sw.DecIndent()
307

    
308
      wrote_opt = True
309

    
310
    if wrote_opt:
311
      sw.Write("fi")
312

    
313
    return
314

    
315
  def _CompleteArguments(self, sw):
316
    if not (self.opts or self.args):
317
      return
318

    
319
    all_option_names = []
320
    for opt in self.opts:
321
      all_option_names.extend(opt.all_names)
322
    all_option_names.sort()
323

    
324
    # List options if no argument has been specified yet
325
    sw.Write("_ganeti_list_options %s",
326
             utils.ShellQuote(" ".join(all_option_names)))
327

    
328
    if self.args:
329
      last_idx = len(self.args) - 1
330
      last_arg_end = 0
331
      varlen_arg_idx = None
332
      wrote_arg = False
333

    
334
      # Write some debug comments
335
      for idx, arg in enumerate(self.args):
336
        sw.Write("# %s: %r", idx, arg)
337

    
338
      sw.Write("compgenargs=")
339

    
340
      for idx, arg in enumerate(self.args):
341
        assert arg.min is not None and arg.min >= 0
342
        assert not (idx < last_idx and arg.max is None)
343

    
344
        if arg.min != arg.max or arg.max is None:
345
          if varlen_arg_idx is not None:
346
            raise Exception("Only one argument can have a variable length")
347
          varlen_arg_idx = idx
348

    
349
        compgenargs = []
350

    
351
        if isinstance(arg, cli.ArgUnknown):
352
          choices = ""
353
        elif isinstance(arg, cli.ArgSuggest):
354
          choices = utils.ShellQuote(" ".join(arg.choices))
355
        elif isinstance(arg, cli.ArgInstance):
356
          choices = "$(_ganeti_instances)"
357
        elif isinstance(arg, cli.ArgNode):
358
          choices = "$(_ganeti_nodes)"
359
        elif isinstance(arg, cli.ArgJobId):
360
          choices = "$(_ganeti_jobs)"
361
        elif isinstance(arg, cli.ArgFile):
362
          choices = ""
363
          compgenargs.append("-f")
364
        elif isinstance(arg, cli.ArgCommand):
365
          choices = ""
366
          compgenargs.append("-c")
367
        elif isinstance(arg, cli.ArgHost):
368
          choices = ""
369
          compgenargs.append("-A hostname")
370
        else:
371
          raise Exception("Unknown argument type %r" % arg)
372

    
373
        if arg.min == 1 and arg.max == 1:
374
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
375
        elif arg.min <= arg.max:
376
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
377
                     (last_arg_end, last_arg_end + arg.max))
378
        elif arg.max is None:
379
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
380
        else:
381
          raise Exception("Unable to generate argument position condition")
382

    
383
        last_arg_end += arg.min
384

    
385
        if choices or compgenargs:
386
          if wrote_arg:
387
            condcmd = "elif"
388
          else:
389
            condcmd = "if"
390

    
391
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
392
          sw.IncIndent()
393
          try:
394
            if choices:
395
              sw.Write("""choices="$choices "%s""", choices)
396
            if compgenargs:
397
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
398
          finally:
399
            sw.DecIndent()
400

    
401
          wrote_arg = True
402

    
403
      if wrote_arg:
404
        sw.Write("fi")
405

    
406
    if self.args:
407
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
408
    else:
409
      # $compgenargs exists only if there are arguments
410
      WriteCompReply(sw, '-W "$choices"')
411

    
412
  def WriteTo(self, sw):
413
    self._FindFirstArgument(sw)
414
    self._CompleteOptionValues(sw)
415
    self._CompleteArguments(sw)
416

    
417

    
418
def WriteCompletion(sw, scriptname, funcname,
419
                    commands=None,
420
                    opts=None, args=None):
421
  """Writes the completion code for one command.
422

    
423
  @type sw: ShellWriter
424
  @param sw: Script writer
425
  @type scriptname: string
426
  @param scriptname: Name of command line program
427
  @type funcname: string
428
  @param funcname: Shell function name
429
  @type commands: list
430
  @param commands: List of all subcommands in this program
431

    
432
  """
433
  sw.Write("%s() {", funcname)
434
  sw.IncIndent()
435
  try:
436
    sw.Write("local "
437
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
438
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
439
             ' i first_arg_idx choices compgenargs arg_idx optcur')
440

    
441
    # Useful for debugging:
442
    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
443
    #sw.Write("set | grep ^COMP_")
444

    
445
    sw.Write("COMPREPLY=()")
446

    
447
    if opts is not None and args is not None:
448
      assert not commands
449
      CompletionWriter(0, opts, args).WriteTo(sw)
450

    
451
    else:
452
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
453
      sw.IncIndent()
454
      try:
455
        # Complete the command name
456
        WriteCompReply(sw,
457
                       ("-W %s" %
458
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
459
      finally:
460
        sw.DecIndent()
461
      sw.Write("fi")
462

    
463
      # We're doing options and arguments to commands
464
      sw.Write("""case "${COMP_WORDS[1]}" in""")
465
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
466
        if not (argdef or optdef):
467
          continue
468

    
469
        # TODO: Group by arguments and options
470
        sw.Write("%s)", utils.ShellQuote(cmd))
471
        sw.IncIndent()
472
        try:
473
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
474
        finally:
475
          sw.DecIndent()
476

    
477
        sw.Write(";;")
478
      sw.Write("esac")
479
  finally:
480
    sw.DecIndent()
481
  sw.Write("}")
482

    
483
  sw.Write("complete -F %s -o filenames %s",
484
           utils.ShellQuote(funcname),
485
           utils.ShellQuote(scriptname))
486

    
487

    
488
def GetFunctionName(name):
489
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
490

    
491

    
492
def LoadModule(filename):
493
  """Loads an external module by filename.
494

    
495
  """
496
  (name, ext) = os.path.splitext(filename)
497

    
498
  fh = open(filename, "U")
499
  try:
500
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
501
  finally:
502
    fh.close()
503

    
504

    
505
def GetCommands(filename, module):
506
  """Returns the commands defined in a module.
507

    
508
  Aliases are also added as commands.
509

    
510
  """
511
  try:
512
    commands = getattr(module, "commands")
513
  except AttributeError, err:
514
    raise Exception("Script %s doesn't have 'commands' attribute" %
515
                    filename)
516

    
517
  # Use aliases
518
  aliases = getattr(module, "aliases", {})
519
  if aliases:
520
    commands = commands.copy()
521
    for name, target in aliases.iteritems():
522
      commands[name] = commands[target]
523

    
524
  return commands
525

    
526

    
527
def main():
528
  buf = StringIO()
529
  sw = ShellWriter(buf)
530

    
531
  WritePreamble(sw)
532

    
533
  # gnt-* scripts
534
  for scriptname in _autoconf.GNT_SCRIPTS:
535
    filename = "scripts/%s" % scriptname
536

    
537
    WriteCompletion(sw, scriptname,
538
                    GetFunctionName(scriptname),
539
                    commands=GetCommands(filename, LoadModule(filename)))
540

    
541
  # Burnin script
542
  burnin = LoadModule("tools/burnin")
543
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
544
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
545

    
546
  print buf.getvalue()
547

    
548

    
549
if __name__ == "__main__":
550
  main()