Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ d4b94fe8

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

    
206
    sw.Write("return 1")
207
  finally:
208
    sw.DecIndent()
209
  sw.Write("}")
210

    
211

    
212
def WriteCompReply(sw, args, cur="\"$cur\""):
213
  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
214
  sw.Write("return")
215

    
216

    
217
class CompletionWriter:
218
  """Command completion writer class.
219

    
220
  """
221
  def __init__(self, arg_offset, opts, args):
222
    self.arg_offset = arg_offset
223
    self.opts = opts
224
    self.args = args
225

    
226
    for opt in opts:
227
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
228

    
229
  def _FindFirstArgument(self, sw):
230
    ignore = []
231
    skip_one = []
232

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

    
243
    ignore = sorted(utils.UniqueSequence(ignore))
244
    skip_one = sorted(utils.UniqueSequence(skip_one))
245

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

    
257
  def _CompleteOptionValues(self, sw):
258
    # Group by values
259
    # "values" -> [optname1, optname2, ...]
260
    values = {}
261

    
262
    for opt in self.opts:
263
      if not opt.takes_value():
264
        continue
265

    
266
      # Only static choices implemented so far (e.g. no node list)
267
      suggest = getattr(opt, "completion_suggest", None)
268

    
269
      if not suggest:
270
        suggest = opt.choices
271

    
272
      if suggest:
273
        suggest_text = " ".join(sorted(suggest))
274
      else:
275
        suggest_text = ""
276

    
277
      values.setdefault(suggest_text, []).extend(opt.all_names)
278

    
279
    # Don't write any code if there are no option values
280
    if not values:
281
      return
282

    
283
    cur = "\"$optcur\""
284

    
285
    wrote_opt = False
286

    
287
    for (suggest, allnames) in values.iteritems():
288
      longnames = [i for i in allnames if i.startswith("--")]
289

    
290
      if wrote_opt:
291
        condcmd = "elif"
292
      else:
293
        condcmd = "if"
294

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

    
304
      wrote_opt = True
305

    
306
    if wrote_opt:
307
      sw.Write("fi")
308

    
309
    return
310

    
311
  def _CompleteArguments(self, sw):
312
    if not (self.opts or self.args):
313
      return
314

    
315
    all_option_names = []
316
    for opt in self.opts:
317
      all_option_names.extend(opt.all_names)
318
    all_option_names.sort()
319

    
320
    # List options if no argument has been specified yet
321
    sw.Write("_ganeti_list_options %s",
322
             utils.ShellQuote(" ".join(all_option_names)))
323

    
324
    if self.args:
325
      last_idx = len(self.args) - 1
326
      last_arg_end = 0
327
      varlen_arg_idx = None
328
      wrote_arg = False
329

    
330
      # Write some debug comments
331
      for idx, arg in enumerate(self.args):
332
        sw.Write("# %s: %r", idx, arg)
333

    
334
      sw.Write("compgenargs=")
335

    
336
      for idx, arg in enumerate(self.args):
337
        assert arg.min is not None and arg.min >= 0
338
        assert not (idx < last_idx and arg.max is None)
339

    
340
        if arg.min != arg.max or arg.max is None:
341
          if varlen_arg_idx is not None:
342
            raise Exception("Only one argument can have a variable length")
343
          varlen_arg_idx = idx
344

    
345
        compgenargs = []
346

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

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

    
379
        last_arg_end += arg.min
380

    
381
        if choices or compgenargs:
382
          if wrote_arg:
383
            condcmd = "elif"
384
          else:
385
            condcmd = "if"
386

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

    
397
          wrote_arg = True
398

    
399
      if wrote_arg:
400
        sw.Write("fi")
401

    
402
    if self.args:
403
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
404
    else:
405
      # $compgenargs exists only if there are arguments
406
      WriteCompReply(sw, '-W "$choices"')
407

    
408
  def WriteTo(self, sw):
409
    self._FindFirstArgument(sw)
410
    self._CompleteOptionValues(sw)
411
    self._CompleteArguments(sw)
412

    
413

    
414
def WriteCompletion(sw, scriptname, funcname,
415
                    commands=None,
416
                    opts=None, args=None):
417
  """Writes the completion code for one command.
418

    
419
  @type sw: ShellWriter
420
  @param sw: Script writer
421
  @type scriptname: string
422
  @param scriptname: Name of command line program
423
  @type funcname: string
424
  @param funcname: Shell function name
425
  @type commands: list
426
  @param commands: List of all subcommands in this program
427

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

    
437
    # Useful for debugging:
438
    #sw.Write("echo cur=\"$cur\" prev=\"$prev\"")
439
    #sw.Write("set | grep ^COMP_")
440

    
441
    sw.Write("COMPREPLY=()")
442

    
443
    if opts is not None and args is not None:
444
      assert not commands
445
      CompletionWriter(0, opts, args).WriteTo(sw)
446

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

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

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

    
473
        sw.Write(";;")
474
      sw.Write("esac")
475
  finally:
476
    sw.DecIndent()
477
  sw.Write("}")
478

    
479
  sw.Write("complete -F %s -o filenames %s",
480
           utils.ShellQuote(funcname),
481
           utils.ShellQuote(scriptname))
482

    
483

    
484
def GetFunctionName(name):
485
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
486

    
487

    
488
def LoadModule(filename):
489
  """Loads an external module by filename.
490

    
491
  """
492
  (name, ext) = os.path.splitext(filename)
493

    
494
  fh = open(filename, "U")
495
  try:
496
    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
497
  finally:
498
    fh.close()
499

    
500

    
501
def GetCommands(filename, module):
502
  """Returns the commands defined in a module.
503

    
504
  Aliases are also added as commands.
505

    
506
  """
507
  try:
508
    commands = getattr(module, "commands")
509
  except AttributeError, err:
510
    raise Exception("Script %s doesn't have 'commands' attribute" %
511
                    filename)
512

    
513
  # Use aliases
514
  aliases = getattr(module, "aliases", {})
515
  if aliases:
516
    commands = commands.copy()
517
    for name, target in aliases.iteritems():
518
      commands[name] = commands[target]
519

    
520
  return commands
521

    
522

    
523
def main():
524
  buf = StringIO()
525
  sw = ShellWriter(buf)
526

    
527
  WritePreamble(sw)
528

    
529
  # gnt-* scripts
530
  for scriptname in _autoconf.GNT_SCRIPTS:
531
    filename = "scripts/%s" % scriptname
532

    
533
    WriteCompletion(sw, scriptname,
534
                    GetFunctionName(scriptname),
535
                    commands=GetCommands(filename, LoadModule(filename)))
536

    
537
  # Burnin script
538
  burnin = LoadModule("tools/burnin")
539
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
540
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
541

    
542
  print buf.getvalue()
543

    
544

    
545
if __name__ == "__main__":
546
  main()