Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 63d44c55

History | View | Annotate | Download (15.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 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 (isinstance(suggest, (int, long)) and
277
          suggest in cli.OPT_COMPL_ALL):
278
        key = suggest
279
      elif suggest:
280
        key = " ".join(sorted(suggest))
281
      else:
282
        key = ""
283

    
284
      values.setdefault(key, []).extend(opt.all_names)
285

    
286
    # Don't write any code if there are no option values
287
    if not values:
288
      return
289

    
290
    cur = "\"$optcur\""
291

    
292
    wrote_opt = False
293

    
294
    for (suggest, allnames) in values.iteritems():
295
      longnames = [i for i in allnames if i.startswith("--")]
296

    
297
      if wrote_opt:
298
        condcmd = "elif"
299
      else:
300
        condcmd = "if"
301

    
302
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
303
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
304
               utils.ShellQuote("|".join(allnames)))
305
      sw.IncIndent()
306
      try:
307
        if suggest == cli.OPT_COMPL_MANY_NODES:
308
          # TODO: Implement comma-separated values
309
          WriteCompReply(sw, "-W ''", cur=cur)
310
        elif suggest == cli.OPT_COMPL_ONE_NODE:
311
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
312
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
313
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
314
        elif suggest == cli.OPT_COMPL_ONE_OS:
315
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
316
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
317
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
318
        else:
319
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
320
      finally:
321
        sw.DecIndent()
322

    
323
      wrote_opt = True
324

    
325
    if wrote_opt:
326
      sw.Write("fi")
327

    
328
    return
329

    
330
  def _CompleteArguments(self, sw):
331
    if not (self.opts or self.args):
332
      return
333

    
334
    all_option_names = []
335
    for opt in self.opts:
336
      all_option_names.extend(opt.all_names)
337
    all_option_names.sort()
338

    
339
    # List options if no argument has been specified yet
340
    sw.Write("_ganeti_list_options %s",
341
             utils.ShellQuote(" ".join(all_option_names)))
342

    
343
    if self.args:
344
      last_idx = len(self.args) - 1
345
      last_arg_end = 0
346
      varlen_arg_idx = None
347
      wrote_arg = False
348

    
349
      # Write some debug comments
350
      for idx, arg in enumerate(self.args):
351
        sw.Write("# %s: %r", idx, arg)
352

    
353
      sw.Write("compgenargs=")
354

    
355
      for idx, arg in enumerate(self.args):
356
        assert arg.min is not None and arg.min >= 0
357
        assert not (idx < last_idx and arg.max is None)
358

    
359
        if arg.min != arg.max or arg.max is None:
360
          if varlen_arg_idx is not None:
361
            raise Exception("Only one argument can have a variable length")
362
          varlen_arg_idx = idx
363

    
364
        compgenargs = []
365

    
366
        if isinstance(arg, cli.ArgUnknown):
367
          choices = ""
368
        elif isinstance(arg, cli.ArgSuggest):
369
          choices = utils.ShellQuote(" ".join(arg.choices))
370
        elif isinstance(arg, cli.ArgInstance):
371
          choices = "$(_ganeti_instances)"
372
        elif isinstance(arg, cli.ArgNode):
373
          choices = "$(_ganeti_nodes)"
374
        elif isinstance(arg, cli.ArgJobId):
375
          choices = "$(_ganeti_jobs)"
376
        elif isinstance(arg, cli.ArgFile):
377
          choices = ""
378
          compgenargs.append("-f")
379
        elif isinstance(arg, cli.ArgCommand):
380
          choices = ""
381
          compgenargs.append("-c")
382
        elif isinstance(arg, cli.ArgHost):
383
          choices = ""
384
          compgenargs.append("-A hostname")
385
        else:
386
          raise Exception("Unknown argument type %r" % arg)
387

    
388
        if arg.min == 1 and arg.max == 1:
389
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
390
        elif arg.min <= arg.max:
391
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
392
                     (last_arg_end, last_arg_end + arg.max))
393
        elif arg.max is None:
394
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
395
        else:
396
          raise Exception("Unable to generate argument position condition")
397

    
398
        last_arg_end += arg.min
399

    
400
        if choices or compgenargs:
401
          if wrote_arg:
402
            condcmd = "elif"
403
          else:
404
            condcmd = "if"
405

    
406
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
407
          sw.IncIndent()
408
          try:
409
            if choices:
410
              sw.Write("""choices="$choices "%s""", choices)
411
            if compgenargs:
412
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
413
          finally:
414
            sw.DecIndent()
415

    
416
          wrote_arg = True
417

    
418
      if wrote_arg:
419
        sw.Write("fi")
420

    
421
    if self.args:
422
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
423
    else:
424
      # $compgenargs exists only if there are arguments
425
      WriteCompReply(sw, '-W "$choices"')
426

    
427
  def WriteTo(self, sw):
428
    self._FindFirstArgument(sw)
429
    self._CompleteOptionValues(sw)
430
    self._CompleteArguments(sw)
431

    
432

    
433
def WriteCompletion(sw, scriptname, funcname,
434
                    commands=None,
435
                    opts=None, args=None):
436
  """Writes the completion code for one command.
437

    
438
  @type sw: ShellWriter
439
  @param sw: Script writer
440
  @type scriptname: string
441
  @param scriptname: Name of command line program
442
  @type funcname: string
443
  @param funcname: Shell function name
444
  @type commands: list
445
  @param commands: List of all subcommands in this program
446

    
447
  """
448
  sw.Write("%s() {", funcname)
449
  sw.IncIndent()
450
  try:
451
    sw.Write("local "
452
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
453
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
454
             ' i first_arg_idx choices compgenargs arg_idx optcur')
455

    
456
    # Useful for debugging:
457
    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
458
    #sw.Write("set | grep ^COMP_")
459

    
460
    sw.Write("COMPREPLY=()")
461

    
462
    if opts is not None and args is not None:
463
      assert not commands
464
      CompletionWriter(0, opts, args).WriteTo(sw)
465

    
466
    else:
467
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
468
      sw.IncIndent()
469
      try:
470
        # Complete the command name
471
        WriteCompReply(sw,
472
                       ("-W %s" %
473
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
474
      finally:
475
        sw.DecIndent()
476
      sw.Write("fi")
477

    
478
      # We're doing options and arguments to commands
479
      sw.Write("""case "${COMP_WORDS[1]}" in""")
480
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
481
        if not (argdef or optdef):
482
          continue
483

    
484
        # TODO: Group by arguments and options
485
        sw.Write("%s)", utils.ShellQuote(cmd))
486
        sw.IncIndent()
487
        try:
488
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
489
        finally:
490
          sw.DecIndent()
491

    
492
        sw.Write(";;")
493
      sw.Write("esac")
494
  finally:
495
    sw.DecIndent()
496
  sw.Write("}")
497

    
498
  sw.Write("complete -F %s -o filenames %s",
499
           utils.ShellQuote(funcname),
500
           utils.ShellQuote(scriptname))
501

    
502

    
503
def GetFunctionName(name):
504
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
505

    
506

    
507
def LoadModule(filename):
508
  """Loads an external module by filename.
509

    
510
  """
511
  (name, ext) = os.path.splitext(filename)
512

    
513
  fh = open(filename, "U")
514
  try:
515
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
516
  finally:
517
    fh.close()
518

    
519

    
520
def GetCommands(filename, module):
521
  """Returns the commands defined in a module.
522

    
523
  Aliases are also added as commands.
524

    
525
  """
526
  try:
527
    commands = getattr(module, "commands")
528
  except AttributeError, err:
529
    raise Exception("Script %s doesn't have 'commands' attribute" %
530
                    filename)
531

    
532
  # Use aliases
533
  aliases = getattr(module, "aliases", {})
534
  if aliases:
535
    commands = commands.copy()
536
    for name, target in aliases.iteritems():
537
      commands[name] = commands[target]
538

    
539
  return commands
540

    
541

    
542
def main():
543
  buf = StringIO()
544
  sw = ShellWriter(buf)
545

    
546
  WritePreamble(sw)
547

    
548
  # gnt-* scripts
549
  for scriptname in _autoconf.GNT_SCRIPTS:
550
    filename = "scripts/%s" % scriptname
551

    
552
    WriteCompletion(sw, scriptname,
553
                    GetFunctionName(scriptname),
554
                    commands=GetCommands(filename, LoadModule(filename)))
555

    
556
  # Burnin script
557
  burnin = LoadModule("tools/burnin")
558
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
559
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
560

    
561
  print buf.getvalue()
562

    
563

    
564
if __name__ == "__main__":
565
  main()