Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 5786c087

History | View | Annotate | Download (17.3 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
"""Script to generate bash_completion script for Ganeti.
23

    
24
"""
25

    
26
# pylint: disable-msg=C0103
27
# [C0103] Invalid name build-bash-completion
28

    
29
import os
30
import re
31
from cStringIO import StringIO
32

    
33
from ganeti import constants
34
from ganeti import cli
35
from ganeti import utils
36
from ganeti import build
37

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

    
42

    
43
def WritePreamble(sw):
44
  """Writes the script preamble.
45

    
46
  Helper functions should be written here.
47

    
48
  """
49
  sw.Write("# This script is automatically generated at build time.")
50
  sw.Write("# Do not modify manually.")
51

    
52
  sw.Write("_ganeti_dbglog() {")
53
  sw.IncIndent()
54
  try:
55
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
56
    sw.IncIndent()
57
    try:
58
      sw.Write("{")
59
      sw.IncIndent()
60
      try:
61
        sw.Write("echo ---")
62
        sw.Write("echo \"$@\"")
63
        sw.Write("echo")
64
      finally:
65
        sw.DecIndent()
66
      sw.Write("} >> $GANETI_COMPL_LOG")
67
    finally:
68
      sw.DecIndent()
69
    sw.Write("fi")
70
  finally:
71
    sw.DecIndent()
72
  sw.Write("}")
73

    
74
  sw.Write("_ganeti_nodes() {")
75
  sw.IncIndent()
76
  try:
77
    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
78
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
79
  finally:
80
    sw.DecIndent()
81
  sw.Write("}")
82

    
83
  sw.Write("_ganeti_instances() {")
84
  sw.IncIndent()
85
  try:
86
    instance_list_path = os.path.join(constants.DATA_DIR,
87
                                      "ssconf_instance_list")
88
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
89
  finally:
90
    sw.DecIndent()
91
  sw.Write("}")
92

    
93
  sw.Write("_ganeti_jobs() {")
94
  sw.IncIndent()
95
  try:
96
    # FIXME: this is really going into the internals of the job queue
97
    sw.Write(("local jlist=$( shopt -s nullglob &&"
98
              " cd %s 2>/dev/null && echo job-* || : )"),
99
             utils.ShellQuote(constants.QUEUE_DIR))
100
    sw.Write('echo "${jlist//job-/}"')
101
  finally:
102
    sw.DecIndent()
103
  sw.Write("}")
104

    
105
  for (fnname, paths) in [
106
      ("os", constants.OS_SEARCH_PATH),
107
      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
108
      ]:
109
    sw.Write("_ganeti_%s() {", fnname)
110
    sw.IncIndent()
111
    try:
112
      # FIXME: Make querying the master for all OSes cheap
113
      for path in paths:
114
        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
115
                 utils.ShellQuote(path))
116
    finally:
117
      sw.DecIndent()
118
    sw.Write("}")
119

    
120
  # Params: <offset> <options with values> <options without values>
121
  # Result variable: $first_arg_idx
122
  sw.Write("_ganeti_find_first_arg() {")
123
  sw.IncIndent()
124
  try:
125
    sw.Write("local w i")
126

    
127
    sw.Write("first_arg_idx=")
128
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
129
    sw.IncIndent()
130
    try:
131
      sw.Write("w=${COMP_WORDS[$i]}")
132

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

    
136
      # Skip
137
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
138

    
139
      # Ah, we found the first argument
140
      sw.Write("else first_arg_idx=$i; break;")
141
      sw.Write("fi")
142
    finally:
143
      sw.DecIndent()
144
    sw.Write("done")
145
  finally:
146
    sw.DecIndent()
147
  sw.Write("}")
148

    
149
  # Params: <list of options separated by space>
150
  # Input variable: $first_arg_idx
151
  # Result variables: $arg_idx, $choices
152
  sw.Write("_ganeti_list_options() {")
153
  sw.IncIndent()
154
  try:
155
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
156
    sw.IncIndent()
157
    try:
158
      sw.Write("arg_idx=0")
159
      # Show options only if the current word starts with a dash
160
      sw.Write("""if [[ "$cur" == -* ]]; then""")
161
      sw.IncIndent()
162
      try:
163
        sw.Write("choices=$1")
164
      finally:
165
        sw.DecIndent()
166
      sw.Write("fi")
167
      sw.Write("return")
168
    finally:
169
      sw.DecIndent()
170
    sw.Write("fi")
171

    
172
    # Calculate position of current argument
173
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
174
    sw.Write("choices=")
175
  finally:
176
    sw.DecIndent()
177
  sw.Write("}")
178

    
179
  # Params: <long options with equal sign> <all options>
180
  # Result variable: $optcur
181
  sw.Write("_ganeti_checkopt() {")
182
  sw.IncIndent()
183
  try:
184
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
185
    sw.IncIndent()
186
    try:
187
      sw.Write("optcur=\"${cur#--*=}\"")
188
      sw.Write("return 0")
189
    finally:
190
      sw.DecIndent()
191
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
192
    sw.IncIndent()
193
    try:
194
      sw.Write("optcur=\"$cur\"")
195
      sw.Write("return 0")
196
    finally:
197
      sw.DecIndent()
198
    sw.Write("fi")
199

    
200
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
201

    
202
    sw.Write("return 1")
203
  finally:
204
    sw.DecIndent()
205
  sw.Write("}")
206

    
207
  # Params: <compgen options>
208
  # Result variable: $COMPREPLY
209
  sw.Write("_ganeti_compgen() {")
210
  sw.IncIndent()
211
  try:
212
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
213
    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
214
  finally:
215
    sw.DecIndent()
216
  sw.Write("}")
217

    
218

    
219
def WriteCompReply(sw, args, cur="\"$cur\""):
220
  sw.Write("_ganeti_compgen %s -- %s", args, cur)
221
  sw.Write("return")
222

    
223

    
224
class CompletionWriter:
225
  """Command completion writer class.
226

    
227
  """
228
  def __init__(self, arg_offset, opts, args):
229
    self.arg_offset = arg_offset
230
    self.opts = opts
231
    self.args = args
232

    
233
    for opt in opts:
234
      # While documented, these variables aren't seen as public attributes by
235
      # pylint. pylint: disable-msg=W0212
236
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
237

    
238
  def _FindFirstArgument(self, sw):
239
    ignore = []
240
    skip_one = []
241

    
242
    for opt in self.opts:
243
      if opt.takes_value():
244
        # Ignore value
245
        for i in opt.all_names:
246
          if i.startswith("--"):
247
            ignore.append("%s=*" % utils.ShellQuote(i))
248
          skip_one.append(utils.ShellQuote(i))
249
      else:
250
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
251

    
252
    ignore = sorted(utils.UniqueSequence(ignore))
253
    skip_one = sorted(utils.UniqueSequence(skip_one))
254

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

    
266
  def _CompleteOptionValues(self, sw):
267
    # Group by values
268
    # "values" -> [optname1, optname2, ...]
269
    values = {}
270

    
271
    for opt in self.opts:
272
      if not opt.takes_value():
273
        continue
274

    
275
      # Only static choices implemented so far (e.g. no node list)
276
      suggest = getattr(opt, "completion_suggest", None)
277

    
278
      # our custom option type
279
      if opt.type == "bool":
280
        suggest = ["yes", "no"]
281

    
282
      if not suggest:
283
        suggest = opt.choices
284

    
285
      if (isinstance(suggest, (int, long)) and
286
          suggest in cli.OPT_COMPL_ALL):
287
        key = suggest
288
      elif suggest:
289
        key = " ".join(sorted(suggest))
290
      else:
291
        key = ""
292

    
293
      values.setdefault(key, []).extend(opt.all_names)
294

    
295
    # Don't write any code if there are no option values
296
    if not values:
297
      return
298

    
299
    cur = "\"$optcur\""
300

    
301
    wrote_opt = False
302

    
303
    for (suggest, allnames) in values.iteritems():
304
      longnames = [i for i in allnames if i.startswith("--")]
305

    
306
      if wrote_opt:
307
        condcmd = "elif"
308
      else:
309
        condcmd = "if"
310

    
311
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
312
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
313
               utils.ShellQuote("|".join(allnames)))
314
      sw.IncIndent()
315
      try:
316
        if suggest == cli.OPT_COMPL_MANY_NODES:
317
          # TODO: Implement comma-separated values
318
          WriteCompReply(sw, "-W ''", cur=cur)
319
        elif suggest == cli.OPT_COMPL_ONE_NODE:
320
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
321
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
322
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
323
        elif suggest == cli.OPT_COMPL_ONE_OS:
324
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
325
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
326
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
327
        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
328
          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
329

    
330
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
331
          sw.IncIndent()
332
          try:
333
            sw.Write("node1=\"${optcur%%:*}\"")
334

    
335
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
336
            sw.IncIndent()
337
            try:
338
              sw.Write("pfx=\"$node1:\"")
339
            finally:
340
              sw.DecIndent()
341
            sw.Write("fi")
342
          finally:
343
            sw.DecIndent()
344
          sw.Write("fi")
345

    
346
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
347
                   " node1=\"'$node1'\"")
348

    
349
          sw.Write("for i in $(_ganeti_nodes); do")
350
          sw.IncIndent()
351
          try:
352
            sw.Write("if [[ -z \"$node1\" ]]; then")
353
            sw.IncIndent()
354
            try:
355
              sw.Write("tmp=\"$tmp $i $i:\"")
356
            finally:
357
              sw.DecIndent()
358
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
359
            sw.IncIndent()
360
            try:
361
              sw.Write("tmp=\"$tmp $i\"")
362
            finally:
363
              sw.DecIndent()
364
            sw.Write("fi")
365
          finally:
366
            sw.DecIndent()
367
          sw.Write("done")
368

    
369
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
370
        else:
371
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
372
      finally:
373
        sw.DecIndent()
374

    
375
      wrote_opt = True
376

    
377
    if wrote_opt:
378
      sw.Write("fi")
379

    
380
    return
381

    
382
  def _CompleteArguments(self, sw):
383
    if not (self.opts or self.args):
384
      return
385

    
386
    all_option_names = []
387
    for opt in self.opts:
388
      all_option_names.extend(opt.all_names)
389
    all_option_names.sort()
390

    
391
    # List options if no argument has been specified yet
392
    sw.Write("_ganeti_list_options %s",
393
             utils.ShellQuote(" ".join(all_option_names)))
394

    
395
    if self.args:
396
      last_idx = len(self.args) - 1
397
      last_arg_end = 0
398
      varlen_arg_idx = None
399
      wrote_arg = False
400

    
401
      # Write some debug comments
402
      for idx, arg in enumerate(self.args):
403
        sw.Write("# %s: %r", idx, arg)
404

    
405
      sw.Write("compgenargs=")
406

    
407
      for idx, arg in enumerate(self.args):
408
        assert arg.min is not None and arg.min >= 0
409
        assert not (idx < last_idx and arg.max is None)
410

    
411
        if arg.min != arg.max or arg.max is None:
412
          if varlen_arg_idx is not None:
413
            raise Exception("Only one argument can have a variable length")
414
          varlen_arg_idx = idx
415

    
416
        compgenargs = []
417

    
418
        if isinstance(arg, cli.ArgUnknown):
419
          choices = ""
420
        elif isinstance(arg, cli.ArgSuggest):
421
          choices = utils.ShellQuote(" ".join(arg.choices))
422
        elif isinstance(arg, cli.ArgInstance):
423
          choices = "$(_ganeti_instances)"
424
        elif isinstance(arg, cli.ArgNode):
425
          choices = "$(_ganeti_nodes)"
426
        elif isinstance(arg, cli.ArgJobId):
427
          choices = "$(_ganeti_jobs)"
428
        elif isinstance(arg, cli.ArgOs):
429
          choices = "$(_ganeti_os)"
430
        elif isinstance(arg, cli.ArgFile):
431
          choices = ""
432
          compgenargs.append("-f")
433
        elif isinstance(arg, cli.ArgCommand):
434
          choices = ""
435
          compgenargs.append("-c")
436
        elif isinstance(arg, cli.ArgHost):
437
          choices = ""
438
          compgenargs.append("-A hostname")
439
        else:
440
          raise Exception("Unknown argument type %r" % arg)
441

    
442
        if arg.min == 1 and arg.max == 1:
443
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
444
        elif arg.max is None:
445
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
446
        elif arg.min <= arg.max:
447
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
448
                     (last_arg_end, last_arg_end + arg.max))
449
        else:
450
          raise Exception("Unable to generate argument position condition")
451

    
452
        last_arg_end += arg.min
453

    
454
        if choices or compgenargs:
455
          if wrote_arg:
456
            condcmd = "elif"
457
          else:
458
            condcmd = "if"
459

    
460
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
461
          sw.IncIndent()
462
          try:
463
            if choices:
464
              sw.Write("""choices="$choices "%s""", choices)
465
            if compgenargs:
466
              sw.Write("compgenargs=%s",
467
                       utils.ShellQuote(" ".join(compgenargs)))
468
          finally:
469
            sw.DecIndent()
470

    
471
          wrote_arg = True
472

    
473
      if wrote_arg:
474
        sw.Write("fi")
475

    
476
    if self.args:
477
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
478
    else:
479
      # $compgenargs exists only if there are arguments
480
      WriteCompReply(sw, '-W "$choices"')
481

    
482
  def WriteTo(self, sw):
483
    self._FindFirstArgument(sw)
484
    self._CompleteOptionValues(sw)
485
    self._CompleteArguments(sw)
486

    
487

    
488
def WriteCompletion(sw, scriptname, funcname,
489
                    commands=None,
490
                    opts=None, args=None):
491
  """Writes the completion code for one command.
492

    
493
  @type sw: ShellWriter
494
  @param sw: Script writer
495
  @type scriptname: string
496
  @param scriptname: Name of command line program
497
  @type funcname: string
498
  @param funcname: Shell function name
499
  @type commands: list
500
  @param commands: List of all subcommands in this program
501

    
502
  """
503
  sw.Write("%s() {", funcname)
504
  sw.IncIndent()
505
  try:
506
    sw.Write("local "
507
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
508
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
509
             ' i first_arg_idx choices compgenargs arg_idx optcur')
510

    
511
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
512
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
513
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
514

    
515
    sw.Write("COMPREPLY=()")
516

    
517
    if opts is not None and args is not None:
518
      assert not commands
519
      CompletionWriter(0, opts, args).WriteTo(sw)
520

    
521
    else:
522
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
523
      sw.IncIndent()
524
      try:
525
        # Complete the command name
526
        WriteCompReply(sw,
527
                       ("-W %s" %
528
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
529
      finally:
530
        sw.DecIndent()
531
      sw.Write("fi")
532

    
533
      # We're doing options and arguments to commands
534
      sw.Write("""case "${COMP_WORDS[1]}" in""")
535
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
536
        if not (argdef or optdef):
537
          continue
538

    
539
        # TODO: Group by arguments and options
540
        sw.Write("%s)", utils.ShellQuote(cmd))
541
        sw.IncIndent()
542
        try:
543
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
544
        finally:
545
          sw.DecIndent()
546

    
547
        sw.Write(";;")
548
      sw.Write("esac")
549
  finally:
550
    sw.DecIndent()
551
  sw.Write("}")
552

    
553
  sw.Write("complete -F %s -o filenames %s",
554
           utils.ShellQuote(funcname),
555
           utils.ShellQuote(scriptname))
556

    
557

    
558
def GetFunctionName(name):
559
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
560

    
561

    
562
def GetCommands(filename, module):
563
  """Returns the commands defined in a module.
564

    
565
  Aliases are also added as commands.
566

    
567
  """
568
  try:
569
    commands = getattr(module, "commands")
570
  except AttributeError:
571
    raise Exception("Script %s doesn't have 'commands' attribute" %
572
                    filename)
573

    
574
  # Add the implicit "--help" option
575
  help_option = cli.cli_option("-h", "--help", default=False,
576
                               action="store_true")
577

    
578
  for name, (_, _, optdef, _, _) in commands.items():
579
    if help_option not in optdef:
580
      optdef.append(help_option)
581
    for opt in cli.COMMON_OPTS:
582
      if opt in optdef:
583
        raise Exception("Common option '%s' listed for command '%s' in %s" %
584
                        (opt, name, filename))
585
      optdef.append(opt)
586

    
587
  # Use aliases
588
  aliases = getattr(module, "aliases", {})
589
  if aliases:
590
    commands = commands.copy()
591
    for name, target in aliases.iteritems():
592
      commands[name] = commands[target]
593

    
594
  return commands
595

    
596

    
597
def main():
598
  buf = StringIO()
599
  sw = utils.ShellWriter(buf)
600

    
601
  WritePreamble(sw)
602

    
603
  # gnt-* scripts
604
  for scriptname in _autoconf.GNT_SCRIPTS:
605
    filename = "scripts/%s" % scriptname
606

    
607
    WriteCompletion(sw, scriptname,
608
                    GetFunctionName(scriptname),
609
                    commands=GetCommands(filename,
610
                                         build.LoadModule(filename)))
611

    
612
  # Burnin script
613
  burnin = build.LoadModule("tools/burnin")
614
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
615
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
616

    
617
  print buf.getvalue()
618

    
619

    
620
if __name__ == "__main__":
621
  main()