Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 632d5090

History | View | Annotate | Download (14.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 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, cur="\"$cur\""):
187
  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
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
      # --foo value
261
      sw.Write("""case "$prev" in""")
262
      for (choices, names) in values.iteritems():
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
    # --foo=value
276
    values_longopts = {}
277

    
278
    for (choices, names) in values.iteritems():
279
      longnames = [i for i in names if i.startswith("--")]
280
      if longnames:
281
        values_longopts[choices] = longnames
282

    
283
    if values_longopts:
284
      sw.Write("""case "$cur" in""")
285
      for (choices, names) in values_longopts.iteritems():
286
        sw.Write("%s)", "|".join([utils.ShellQuote(i) + "=*" for i in names]))
287
        sw.IncIndent()
288
        try:
289
          # Shell expression to get option value
290
          cur="\"${cur#--*=}\""
291
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices), cur=cur)
292
        finally:
293
          sw.DecIndent()
294
        sw.Write(";;")
295
      sw.Write("""esac""")
296

    
297
  def _CompleteArguments(self, sw):
298
    if not (self.opts or self.args):
299
      return
300

    
301
    all_option_names = []
302
    for opt in self.opts:
303
      all_option_names.extend(opt.all_names)
304
    all_option_names.sort()
305

    
306
    # List options if no argument has been specified yet
307
    sw.Write("_ganeti_list_options %s",
308
             utils.ShellQuote(" ".join(all_option_names)))
309

    
310
    if self.args:
311
      last_idx = len(self.args) - 1
312
      last_arg_end = 0
313
      varlen_arg_idx = None
314
      wrote_arg = False
315

    
316
      # Write some debug comments
317
      for idx, arg in enumerate(self.args):
318
        sw.Write("# %s: %r", idx, arg)
319

    
320
      sw.Write("compgenargs=")
321

    
322
      for idx, arg in enumerate(self.args):
323
        assert arg.min is not None and arg.min >= 0
324
        assert not (idx < last_idx and arg.max is None)
325

    
326
        if arg.min != arg.max or arg.max is None:
327
          if varlen_arg_idx is not None:
328
            raise Exception("Only one argument can have a variable length")
329
          varlen_arg_idx = idx
330

    
331
        compgenargs = []
332

    
333
        if isinstance(arg, cli.ArgUnknown):
334
          choices = ""
335
        elif isinstance(arg, cli.ArgSuggest):
336
          choices = utils.ShellQuote(" ".join(arg.choices))
337
        elif isinstance(arg, cli.ArgInstance):
338
          choices = "$(_ganeti_instances)"
339
        elif isinstance(arg, cli.ArgNode):
340
          choices = "$(_ganeti_nodes)"
341
        elif isinstance(arg, cli.ArgJobId):
342
          choices = "$(_ganeti_jobs)"
343
        elif isinstance(arg, cli.ArgFile):
344
          choices = ""
345
          compgenargs.append("-f")
346
        elif isinstance(arg, cli.ArgCommand):
347
          choices = ""
348
          compgenargs.append("-c")
349
        elif isinstance(arg, cli.ArgHost):
350
          choices = ""
351
          compgenargs.append("-A hostname")
352
        else:
353
          raise Exception("Unknown argument type %r" % arg)
354

    
355
        if arg.min == 1 and arg.max == 1:
356
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
357
        elif arg.min <= arg.max:
358
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
359
                     (last_arg_end, last_arg_end + arg.max))
360
        elif arg.max is None:
361
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
362
        else:
363
          raise Exception("Unable to generate argument position condition")
364

    
365
        last_arg_end += arg.min
366

    
367
        if choices or compgenargs:
368
          if wrote_arg:
369
            condcmd = "elif"
370
          else:
371
            condcmd = "if"
372

    
373
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
374
          sw.IncIndent()
375
          try:
376
            if choices:
377
              sw.Write("""choices="$choices "%s""", choices)
378
            if compgenargs:
379
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
380
          finally:
381
            sw.DecIndent()
382

    
383
          wrote_arg = True
384

    
385
      if wrote_arg:
386
        sw.Write("fi")
387

    
388
    if self.args:
389
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
390
    else:
391
      # $compgenargs exists only if there are arguments
392
      WriteCompReply(sw, '-W "$choices"')
393

    
394
  def WriteTo(self, sw):
395
    self._FindFirstArgument(sw)
396
    self._CompleteOptionValues(sw)
397
    self._CompleteArguments(sw)
398

    
399

    
400
def WriteCompletion(sw, scriptname, funcname,
401
                    commands=None,
402
                    opts=None, args=None):
403
  """Writes the completion code for one command.
404

    
405
  @type sw: ShellWriter
406
  @param sw: Script writer
407
  @type scriptname: string
408
  @param scriptname: Name of command line program
409
  @type funcname: string
410
  @param funcname: Shell function name
411
  @type commands: list
412
  @param commands: List of all subcommands in this program
413

    
414
  """
415
  sw.Write("%s() {", funcname)
416
  sw.IncIndent()
417
  try:
418
    sw.Write("local "
419
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
420
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
421
             ' i first_arg_idx choices compgenargs arg_idx')
422

    
423
    # Useful for debugging:
424
    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
425
    #sw.Write("set | grep ^COMP_")
426

    
427
    sw.Write("COMPREPLY=()")
428

    
429
    if opts is not None and args is not None:
430
      assert not commands
431
      CompletionWriter(0, opts, args).WriteTo(sw)
432

    
433
    else:
434
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
435
      sw.IncIndent()
436
      try:
437
        # Complete the command name
438
        WriteCompReply(sw,
439
                       ("-W %s" %
440
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
441
      finally:
442
        sw.DecIndent()
443
      sw.Write("fi")
444

    
445
      # We're doing options and arguments to commands
446
      sw.Write("""case "${COMP_WORDS[1]}" in""")
447
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
448
        if not (argdef or optdef):
449
          continue
450

    
451
        # TODO: Group by arguments and options
452
        sw.Write("%s)", utils.ShellQuote(cmd))
453
        sw.IncIndent()
454
        try:
455
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
456
        finally:
457
          sw.DecIndent()
458

    
459
        sw.Write(";;")
460
      sw.Write("esac")
461
  finally:
462
    sw.DecIndent()
463
  sw.Write("}")
464

    
465
  sw.Write("complete -F %s -o filenames %s",
466
           utils.ShellQuote(funcname),
467
           utils.ShellQuote(scriptname))
468

    
469

    
470
def GetFunctionName(name):
471
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
472

    
473

    
474
def LoadModule(filename):
475
  """Loads an external module by filename.
476

    
477
  """
478
  (name, ext) = os.path.splitext(filename)
479

    
480
  fh = open(filename, "U")
481
  try:
482
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
483
  finally:
484
    fh.close()
485

    
486

    
487
def GetCommands(filename, module):
488
  """Returns the commands defined in a module.
489

    
490
  Aliases are also added as commands.
491

    
492
  """
493
  try:
494
    commands = getattr(module, "commands")
495
  except AttributeError, err:
496
    raise Exception("Script %s doesn't have 'commands' attribute" %
497
                    filename)
498

    
499
  # Use aliases
500
  aliases = getattr(module, "aliases", {})
501
  if aliases:
502
    commands = commands.copy()
503
    for name, target in aliases.iteritems():
504
      commands[name] = commands[target]
505

    
506
  return commands
507

    
508

    
509
def main():
510
  buf = StringIO()
511
  sw = ShellWriter(buf)
512

    
513
  WritePreamble(sw)
514

    
515
  # gnt-* scripts
516
  for scriptname in _autoconf.GNT_SCRIPTS:
517
    filename = "scripts/%s" % scriptname
518

    
519
    WriteCompletion(sw, scriptname,
520
                    GetFunctionName(scriptname),
521
                    commands=GetCommands(filename, LoadModule(filename)))
522

    
523
  # Burnin script
524
  burnin = LoadModule("tools/burnin")
525
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
526
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
527

    
528
  print buf.getvalue()
529

    
530

    
531
if __name__ == "__main__":
532
  main()