Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 5b0ca9d4

History | View | Annotate | Download (15.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 optparse
23
import os
24
import sys
25
import re
26
from cStringIO import StringIO
27

    
28
from ganeti import constants
29
from ganeti import cli
30
from ganeti import utils
31

    
32
# _autoconf shouldn't be imported from anywhere except constants.py, but we're
33
# making an exception here because this script is only used at build time.
34
from ganeti import _autoconf
35

    
36

    
37
class ShellWriter:
38
  """Helper class to write scripts with indentation.
39

    
40
  """
41
  INDENT_STR = "  "
42

    
43
  def __init__(self, fh):
44
    self._fh = fh
45
    self._indent = 0
46

    
47
  def IncIndent(self):
48
    """Increase indentation level by 1.
49

    
50
    """
51
    self._indent += 1
52

    
53
  def DecIndent(self):
54
    """Decrease indentation level by 1.
55

    
56
    """
57
    assert self._indent > 0
58
    self._indent -= 1
59

    
60
  def Write(self, txt, *args):
61
    """Write line to output file.
62

    
63
    """
64
    self._fh.write(self._indent * self.INDENT_STR)
65

    
66
    if args:
67
      self._fh.write(txt % args)
68
    else:
69
      self._fh.write(txt)
70

    
71
    self._fh.write("\n")
72

    
73

    
74
def WritePreamble(sw):
75
  """Writes the script preamble.
76

    
77
  Helper functions should be written here.
78

    
79
  """
80
  sw.Write("# This script is automatically generated at build time.")
81
  sw.Write("# Do not modify manually.")
82

    
83
  sw.Write("_ganeti_dbglog() {")
84
  sw.IncIndent()
85
  try:
86
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
87
    sw.IncIndent()
88
    try:
89
      sw.Write("{")
90
      sw.IncIndent()
91
      try:
92
        sw.Write("echo ---")
93
        sw.Write("echo \"$@\"")
94
        sw.Write("echo")
95
      finally:
96
        sw.DecIndent()
97
      sw.Write("} >> $GANETI_COMPL_LOG")
98
    finally:
99
      sw.DecIndent()
100
    sw.Write("fi")
101
  finally:
102
    sw.DecIndent()
103
  sw.Write("}")
104

    
105
  sw.Write("_ganeti_nodes() {")
106
  sw.IncIndent()
107
  try:
108
    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
109
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
110
  finally:
111
    sw.DecIndent()
112
  sw.Write("}")
113

    
114
  sw.Write("_ganeti_instances() {")
115
  sw.IncIndent()
116
  try:
117
    instance_list_path = os.path.join(constants.DATA_DIR,
118
                                      "ssconf_instance_list")
119
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
120
  finally:
121
    sw.DecIndent()
122
  sw.Write("}")
123

    
124
  sw.Write("_ganeti_jobs() {")
125
  sw.IncIndent()
126
  try:
127
    # FIXME: this is really going into the internals of the job queue
128
    sw.Write(("local jlist=$( shopt -s nullglob &&"
129
              " cd %s 2>/dev/null && echo job-* || : )"),
130
             utils.ShellQuote(constants.QUEUE_DIR))
131
    sw.Write('echo "${jlist//job-/}"')
132
  finally:
133
    sw.DecIndent()
134
  sw.Write("}")
135

    
136
  for (fnname, paths) in [
137
      ("os", constants.OS_SEARCH_PATH),
138
      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
139
      ]:
140
    sw.Write("_ganeti_%s() {", fnname)
141
    sw.IncIndent()
142
    try:
143
      # FIXME: Make querying the master for all OSes cheap
144
      for path in paths:
145
        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
146
                 utils.ShellQuote(path))
147
    finally:
148
      sw.DecIndent()
149
    sw.Write("}")
150

    
151
  # Params: <offset> <options with values> <options without values>
152
  # Result variable: $first_arg_idx
153
  sw.Write("_ganeti_find_first_arg() {")
154
  sw.IncIndent()
155
  try:
156
    sw.Write("local w i")
157

    
158
    sw.Write("first_arg_idx=")
159
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
160
    sw.IncIndent()
161
    try:
162
      sw.Write("w=${COMP_WORDS[$i]}")
163

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

    
167
      # Skip
168
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
169

    
170
      # Ah, we found the first argument
171
      sw.Write("else first_arg_idx=$i; break;")
172
      sw.Write("fi")
173
    finally:
174
      sw.DecIndent()
175
    sw.Write("done")
176
  finally:
177
    sw.DecIndent()
178
  sw.Write("}")
179

    
180
  # Params: <list of options separated by space>
181
  # Input variable: $first_arg_idx
182
  # Result variables: $arg_idx, $choices
183
  sw.Write("_ganeti_list_options() {")
184
  sw.IncIndent()
185
  try:
186
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
187
    sw.IncIndent()
188
    try:
189
      sw.Write("arg_idx=0")
190
      # Show options only if the current word starts with a dash
191
      sw.Write("""if [[ "$cur" == -* ]]; then""")
192
      sw.IncIndent()
193
      try:
194
        sw.Write("choices=$1")
195
      finally:
196
        sw.DecIndent()
197
      sw.Write("fi")
198
      sw.Write("return")
199
    finally:
200
      sw.DecIndent()
201
    sw.Write("fi")
202

    
203
    # Calculate position of current argument
204
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
205
    sw.Write("choices=")
206
  finally:
207
    sw.DecIndent()
208
  sw.Write("}")
209

    
210
  # Params: <long options with equal sign> <all options>
211
  # Result variable: $optcur
212
  sw.Write("_ganeti_checkopt() {")
213
  sw.IncIndent()
214
  try:
215
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
216
    sw.IncIndent()
217
    try:
218
      sw.Write("optcur=\"${cur#--*=}\"")
219
      sw.Write("return 0")
220
    finally:
221
      sw.DecIndent()
222
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
223
    sw.IncIndent()
224
    try:
225
      sw.Write("optcur=\"$cur\"")
226
      sw.Write("return 0")
227
    finally:
228
      sw.DecIndent()
229
    sw.Write("fi")
230

    
231
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
232

    
233
    sw.Write("return 1")
234
  finally:
235
    sw.DecIndent()
236
  sw.Write("}")
237

    
238

    
239
def WriteCompReply(sw, args, cur="\"$cur\""):
240
  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
241
  sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
242
  sw.Write("return")
243

    
244

    
245
class CompletionWriter:
246
  """Command completion writer class.
247

    
248
  """
249
  def __init__(self, arg_offset, opts, args):
250
    self.arg_offset = arg_offset
251
    self.opts = opts
252
    self.args = args
253

    
254
    for opt in opts:
255
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
256

    
257
  def _FindFirstArgument(self, sw):
258
    ignore = []
259
    skip_one = []
260

    
261
    for opt in self.opts:
262
      if opt.takes_value():
263
        # Ignore value
264
        for i in opt.all_names:
265
          if i.startswith("--"):
266
            ignore.append("%s=*" % utils.ShellQuote(i))
267
          skip_one.append(utils.ShellQuote(i))
268
      else:
269
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
270

    
271
    ignore = sorted(utils.UniqueSequence(ignore))
272
    skip_one = sorted(utils.UniqueSequence(skip_one))
273

    
274
    if ignore or skip_one:
275
      # Try to locate first argument
276
      sw.Write("_ganeti_find_first_arg %s %s %s",
277
               self.arg_offset + 1,
278
               utils.ShellQuote("|".join(skip_one)),
279
               utils.ShellQuote("|".join(ignore)))
280
    else:
281
      # When there are no options the first argument is always at position
282
      # offset + 1
283
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
284

    
285
  def _CompleteOptionValues(self, sw):
286
    # Group by values
287
    # "values" -> [optname1, optname2, ...]
288
    values = {}
289

    
290
    for opt in self.opts:
291
      if not opt.takes_value():
292
        continue
293

    
294
      # Only static choices implemented so far (e.g. no node list)
295
      suggest = getattr(opt, "completion_suggest", None)
296

    
297
      if not suggest:
298
        suggest = opt.choices
299

    
300
      if (isinstance(suggest, (int, long)) and
301
          suggest in cli.OPT_COMPL_ALL):
302
        key = suggest
303
      elif suggest:
304
        key = " ".join(sorted(suggest))
305
      else:
306
        key = ""
307

    
308
      values.setdefault(key, []).extend(opt.all_names)
309

    
310
    # Don't write any code if there are no option values
311
    if not values:
312
      return
313

    
314
    cur = "\"$optcur\""
315

    
316
    wrote_opt = False
317

    
318
    for (suggest, allnames) in values.iteritems():
319
      longnames = [i for i in allnames if i.startswith("--")]
320

    
321
      if wrote_opt:
322
        condcmd = "elif"
323
      else:
324
        condcmd = "if"
325

    
326
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
327
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
328
               utils.ShellQuote("|".join(allnames)))
329
      sw.IncIndent()
330
      try:
331
        if suggest == cli.OPT_COMPL_MANY_NODES:
332
          # TODO: Implement comma-separated values
333
          WriteCompReply(sw, "-W ''", cur=cur)
334
        elif suggest == cli.OPT_COMPL_ONE_NODE:
335
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
336
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
337
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
338
        elif suggest == cli.OPT_COMPL_ONE_OS:
339
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
340
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
341
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
342
        else:
343
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
344
      finally:
345
        sw.DecIndent()
346

    
347
      wrote_opt = True
348

    
349
    if wrote_opt:
350
      sw.Write("fi")
351

    
352
    return
353

    
354
  def _CompleteArguments(self, sw):
355
    if not (self.opts or self.args):
356
      return
357

    
358
    all_option_names = []
359
    for opt in self.opts:
360
      all_option_names.extend(opt.all_names)
361
    all_option_names.sort()
362

    
363
    # List options if no argument has been specified yet
364
    sw.Write("_ganeti_list_options %s",
365
             utils.ShellQuote(" ".join(all_option_names)))
366

    
367
    if self.args:
368
      last_idx = len(self.args) - 1
369
      last_arg_end = 0
370
      varlen_arg_idx = None
371
      wrote_arg = False
372

    
373
      # Write some debug comments
374
      for idx, arg in enumerate(self.args):
375
        sw.Write("# %s: %r", idx, arg)
376

    
377
      sw.Write("compgenargs=")
378

    
379
      for idx, arg in enumerate(self.args):
380
        assert arg.min is not None and arg.min >= 0
381
        assert not (idx < last_idx and arg.max is None)
382

    
383
        if arg.min != arg.max or arg.max is None:
384
          if varlen_arg_idx is not None:
385
            raise Exception("Only one argument can have a variable length")
386
          varlen_arg_idx = idx
387

    
388
        compgenargs = []
389

    
390
        if isinstance(arg, cli.ArgUnknown):
391
          choices = ""
392
        elif isinstance(arg, cli.ArgSuggest):
393
          choices = utils.ShellQuote(" ".join(arg.choices))
394
        elif isinstance(arg, cli.ArgInstance):
395
          choices = "$(_ganeti_instances)"
396
        elif isinstance(arg, cli.ArgNode):
397
          choices = "$(_ganeti_nodes)"
398
        elif isinstance(arg, cli.ArgJobId):
399
          choices = "$(_ganeti_jobs)"
400
        elif isinstance(arg, cli.ArgFile):
401
          choices = ""
402
          compgenargs.append("-f")
403
        elif isinstance(arg, cli.ArgCommand):
404
          choices = ""
405
          compgenargs.append("-c")
406
        elif isinstance(arg, cli.ArgHost):
407
          choices = ""
408
          compgenargs.append("-A hostname")
409
        else:
410
          raise Exception("Unknown argument type %r" % arg)
411

    
412
        if arg.min == 1 and arg.max == 1:
413
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
414
        elif arg.min <= arg.max:
415
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
416
                     (last_arg_end, last_arg_end + arg.max))
417
        elif arg.max is None:
418
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
419
        else:
420
          raise Exception("Unable to generate argument position condition")
421

    
422
        last_arg_end += arg.min
423

    
424
        if choices or compgenargs:
425
          if wrote_arg:
426
            condcmd = "elif"
427
          else:
428
            condcmd = "if"
429

    
430
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
431
          sw.IncIndent()
432
          try:
433
            if choices:
434
              sw.Write("""choices="$choices "%s""", choices)
435
            if compgenargs:
436
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
437
          finally:
438
            sw.DecIndent()
439

    
440
          wrote_arg = True
441

    
442
      if wrote_arg:
443
        sw.Write("fi")
444

    
445
    if self.args:
446
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
447
    else:
448
      # $compgenargs exists only if there are arguments
449
      WriteCompReply(sw, '-W "$choices"')
450

    
451
  def WriteTo(self, sw):
452
    self._FindFirstArgument(sw)
453
    self._CompleteOptionValues(sw)
454
    self._CompleteArguments(sw)
455

    
456

    
457
def WriteCompletion(sw, scriptname, funcname,
458
                    commands=None,
459
                    opts=None, args=None):
460
  """Writes the completion code for one command.
461

    
462
  @type sw: ShellWriter
463
  @param sw: Script writer
464
  @type scriptname: string
465
  @param scriptname: Name of command line program
466
  @type funcname: string
467
  @param funcname: Shell function name
468
  @type commands: list
469
  @param commands: List of all subcommands in this program
470

    
471
  """
472
  sw.Write("%s() {", funcname)
473
  sw.IncIndent()
474
  try:
475
    sw.Write("local "
476
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
477
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
478
             ' i first_arg_idx choices compgenargs arg_idx optcur')
479

    
480
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
481
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
482
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
483

    
484
    sw.Write("COMPREPLY=()")
485

    
486
    if opts is not None and args is not None:
487
      assert not commands
488
      CompletionWriter(0, opts, args).WriteTo(sw)
489

    
490
    else:
491
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
492
      sw.IncIndent()
493
      try:
494
        # Complete the command name
495
        WriteCompReply(sw,
496
                       ("-W %s" %
497
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
498
      finally:
499
        sw.DecIndent()
500
      sw.Write("fi")
501

    
502
      # We're doing options and arguments to commands
503
      sw.Write("""case "${COMP_WORDS[1]}" in""")
504
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
505
        if not (argdef or optdef):
506
          continue
507

    
508
        # TODO: Group by arguments and options
509
        sw.Write("%s)", utils.ShellQuote(cmd))
510
        sw.IncIndent()
511
        try:
512
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
513
        finally:
514
          sw.DecIndent()
515

    
516
        sw.Write(";;")
517
      sw.Write("esac")
518
  finally:
519
    sw.DecIndent()
520
  sw.Write("}")
521

    
522
  sw.Write("complete -F %s -o filenames %s",
523
           utils.ShellQuote(funcname),
524
           utils.ShellQuote(scriptname))
525

    
526

    
527
def GetFunctionName(name):
528
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
529

    
530

    
531
def GetCommands(filename, module):
532
  """Returns the commands defined in a module.
533

    
534
  Aliases are also added as commands.
535

    
536
  """
537
  try:
538
    commands = getattr(module, "commands")
539
  except AttributeError, err:
540
    raise Exception("Script %s doesn't have 'commands' attribute" %
541
                    filename)
542

    
543
  # Use aliases
544
  aliases = getattr(module, "aliases", {})
545
  if aliases:
546
    commands = commands.copy()
547
    for name, target in aliases.iteritems():
548
      commands[name] = commands[target]
549

    
550
  return commands
551

    
552

    
553
def main():
554
  buf = StringIO()
555
  sw = ShellWriter(buf)
556

    
557
  WritePreamble(sw)
558

    
559
  # gnt-* scripts
560
  for scriptname in _autoconf.GNT_SCRIPTS:
561
    filename = "scripts/%s" % scriptname
562

    
563
    WriteCompletion(sw, scriptname,
564
                    GetFunctionName(scriptname),
565
                    commands=GetCommands(filename,
566
                                         utils.LoadModule(filename)))
567

    
568
  # Burnin script
569
  burnin = utils.LoadModule("tools/burnin")
570
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
571
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
572

    
573
  print buf.getvalue()
574

    
575

    
576
if __name__ == "__main__":
577
  main()