Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 18e2b6e4

History | View | Annotate | Download (17.7 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
  sw.Write("_ganeti_nodegroup() {")
121
  sw.IncIndent()
122
  try:
123
    nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
124
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
125
  finally:
126
    sw.DecIndent()
127
  sw.Write("}")
128

    
129
  # Params: <offset> <options with values> <options without values>
130
  # Result variable: $first_arg_idx
131
  sw.Write("_ganeti_find_first_arg() {")
132
  sw.IncIndent()
133
  try:
134
    sw.Write("local w i")
135

    
136
    sw.Write("first_arg_idx=")
137
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
138
    sw.IncIndent()
139
    try:
140
      sw.Write("w=${COMP_WORDS[$i]}")
141

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

    
145
      # Skip
146
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
147

    
148
      # Ah, we found the first argument
149
      sw.Write("else first_arg_idx=$i; break;")
150
      sw.Write("fi")
151
    finally:
152
      sw.DecIndent()
153
    sw.Write("done")
154
  finally:
155
    sw.DecIndent()
156
  sw.Write("}")
157

    
158
  # Params: <list of options separated by space>
159
  # Input variable: $first_arg_idx
160
  # Result variables: $arg_idx, $choices
161
  sw.Write("_ganeti_list_options() {")
162
  sw.IncIndent()
163
  try:
164
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
165
    sw.IncIndent()
166
    try:
167
      sw.Write("arg_idx=0")
168
      # Show options only if the current word starts with a dash
169
      sw.Write("""if [[ "$cur" == -* ]]; then""")
170
      sw.IncIndent()
171
      try:
172
        sw.Write("choices=$1")
173
      finally:
174
        sw.DecIndent()
175
      sw.Write("fi")
176
      sw.Write("return")
177
    finally:
178
      sw.DecIndent()
179
    sw.Write("fi")
180

    
181
    # Calculate position of current argument
182
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
183
    sw.Write("choices=")
184
  finally:
185
    sw.DecIndent()
186
  sw.Write("}")
187

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

    
209
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
210

    
211
    sw.Write("return 1")
212
  finally:
213
    sw.DecIndent()
214
  sw.Write("}")
215

    
216
  # Params: <compgen options>
217
  # Result variable: $COMPREPLY
218
  sw.Write("_ganeti_compgen() {")
219
  sw.IncIndent()
220
  try:
221
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
222
    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
223
  finally:
224
    sw.DecIndent()
225
  sw.Write("}")
226

    
227

    
228
def WriteCompReply(sw, args, cur="\"$cur\""):
229
  sw.Write("_ganeti_compgen %s -- %s", args, cur)
230
  sw.Write("return")
231

    
232

    
233
class CompletionWriter:
234
  """Command completion writer class.
235

    
236
  """
237
  def __init__(self, arg_offset, opts, args):
238
    self.arg_offset = arg_offset
239
    self.opts = opts
240
    self.args = args
241

    
242
    for opt in opts:
243
      # While documented, these variables aren't seen as public attributes by
244
      # pylint. pylint: disable-msg=W0212
245
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
246

    
247
  def _FindFirstArgument(self, sw):
248
    ignore = []
249
    skip_one = []
250

    
251
    for opt in self.opts:
252
      if opt.takes_value():
253
        # Ignore value
254
        for i in opt.all_names:
255
          if i.startswith("--"):
256
            ignore.append("%s=*" % utils.ShellQuote(i))
257
          skip_one.append(utils.ShellQuote(i))
258
      else:
259
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
260

    
261
    ignore = sorted(utils.UniqueSequence(ignore))
262
    skip_one = sorted(utils.UniqueSequence(skip_one))
263

    
264
    if ignore or skip_one:
265
      # Try to locate first argument
266
      sw.Write("_ganeti_find_first_arg %s %s %s",
267
               self.arg_offset + 1,
268
               utils.ShellQuote("|".join(skip_one)),
269
               utils.ShellQuote("|".join(ignore)))
270
    else:
271
      # When there are no options the first argument is always at position
272
      # offset + 1
273
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
274

    
275
  def _CompleteOptionValues(self, sw):
276
    # Group by values
277
    # "values" -> [optname1, optname2, ...]
278
    values = {}
279

    
280
    for opt in self.opts:
281
      if not opt.takes_value():
282
        continue
283

    
284
      # Only static choices implemented so far (e.g. no node list)
285
      suggest = getattr(opt, "completion_suggest", None)
286

    
287
      # our custom option type
288
      if opt.type == "bool":
289
        suggest = ["yes", "no"]
290

    
291
      if not suggest:
292
        suggest = opt.choices
293

    
294
      if (isinstance(suggest, (int, long)) and
295
          suggest in cli.OPT_COMPL_ALL):
296
        key = suggest
297
      elif suggest:
298
        key = " ".join(sorted(suggest))
299
      else:
300
        key = ""
301

    
302
      values.setdefault(key, []).extend(opt.all_names)
303

    
304
    # Don't write any code if there are no option values
305
    if not values:
306
      return
307

    
308
    cur = "\"$optcur\""
309

    
310
    wrote_opt = False
311

    
312
    for (suggest, allnames) in values.iteritems():
313
      longnames = [i for i in allnames if i.startswith("--")]
314

    
315
      if wrote_opt:
316
        condcmd = "elif"
317
      else:
318
        condcmd = "if"
319

    
320
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
321
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
322
               utils.ShellQuote("|".join(allnames)))
323
      sw.IncIndent()
324
      try:
325
        if suggest == cli.OPT_COMPL_MANY_NODES:
326
          # TODO: Implement comma-separated values
327
          WriteCompReply(sw, "-W ''", cur=cur)
328
        elif suggest == cli.OPT_COMPL_ONE_NODE:
329
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
330
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
331
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
332
        elif suggest == cli.OPT_COMPL_ONE_OS:
333
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
334
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
335
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
336
        elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
337
          WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
338
        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
339
          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
340

    
341
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
342
          sw.IncIndent()
343
          try:
344
            sw.Write("node1=\"${optcur%%:*}\"")
345

    
346
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
347
            sw.IncIndent()
348
            try:
349
              sw.Write("pfx=\"$node1:\"")
350
            finally:
351
              sw.DecIndent()
352
            sw.Write("fi")
353
          finally:
354
            sw.DecIndent()
355
          sw.Write("fi")
356

    
357
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
358
                   " node1=\"'$node1'\"")
359

    
360
          sw.Write("for i in $(_ganeti_nodes); do")
361
          sw.IncIndent()
362
          try:
363
            sw.Write("if [[ -z \"$node1\" ]]; then")
364
            sw.IncIndent()
365
            try:
366
              sw.Write("tmp=\"$tmp $i $i:\"")
367
            finally:
368
              sw.DecIndent()
369
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
370
            sw.IncIndent()
371
            try:
372
              sw.Write("tmp=\"$tmp $i\"")
373
            finally:
374
              sw.DecIndent()
375
            sw.Write("fi")
376
          finally:
377
            sw.DecIndent()
378
          sw.Write("done")
379

    
380
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
381
        else:
382
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
383
      finally:
384
        sw.DecIndent()
385

    
386
      wrote_opt = True
387

    
388
    if wrote_opt:
389
      sw.Write("fi")
390

    
391
    return
392

    
393
  def _CompleteArguments(self, sw):
394
    if not (self.opts or self.args):
395
      return
396

    
397
    all_option_names = []
398
    for opt in self.opts:
399
      all_option_names.extend(opt.all_names)
400
    all_option_names.sort()
401

    
402
    # List options if no argument has been specified yet
403
    sw.Write("_ganeti_list_options %s",
404
             utils.ShellQuote(" ".join(all_option_names)))
405

    
406
    if self.args:
407
      last_idx = len(self.args) - 1
408
      last_arg_end = 0
409
      varlen_arg_idx = None
410
      wrote_arg = False
411

    
412
      # Write some debug comments
413
      for idx, arg in enumerate(self.args):
414
        sw.Write("# %s: %r", idx, arg)
415

    
416
      sw.Write("compgenargs=")
417

    
418
      for idx, arg in enumerate(self.args):
419
        assert arg.min is not None and arg.min >= 0
420
        assert not (idx < last_idx and arg.max is None)
421

    
422
        if arg.min != arg.max or arg.max is None:
423
          if varlen_arg_idx is not None:
424
            raise Exception("Only one argument can have a variable length")
425
          varlen_arg_idx = idx
426

    
427
        compgenargs = []
428

    
429
        if isinstance(arg, cli.ArgUnknown):
430
          choices = ""
431
        elif isinstance(arg, cli.ArgSuggest):
432
          choices = utils.ShellQuote(" ".join(arg.choices))
433
        elif isinstance(arg, cli.ArgInstance):
434
          choices = "$(_ganeti_instances)"
435
        elif isinstance(arg, cli.ArgNode):
436
          choices = "$(_ganeti_nodes)"
437
        elif isinstance(arg, cli.ArgJobId):
438
          choices = "$(_ganeti_jobs)"
439
        elif isinstance(arg, cli.ArgOs):
440
          choices = "$(_ganeti_os)"
441
        elif isinstance(arg, cli.ArgFile):
442
          choices = ""
443
          compgenargs.append("-f")
444
        elif isinstance(arg, cli.ArgCommand):
445
          choices = ""
446
          compgenargs.append("-c")
447
        elif isinstance(arg, cli.ArgHost):
448
          choices = ""
449
          compgenargs.append("-A hostname")
450
        else:
451
          raise Exception("Unknown argument type %r" % arg)
452

    
453
        if arg.min == 1 and arg.max == 1:
454
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
455
        elif arg.max is None:
456
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
457
        elif arg.min <= arg.max:
458
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
459
                     (last_arg_end, last_arg_end + arg.max))
460
        else:
461
          raise Exception("Unable to generate argument position condition")
462

    
463
        last_arg_end += arg.min
464

    
465
        if choices or compgenargs:
466
          if wrote_arg:
467
            condcmd = "elif"
468
          else:
469
            condcmd = "if"
470

    
471
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
472
          sw.IncIndent()
473
          try:
474
            if choices:
475
              sw.Write("""choices="$choices "%s""", choices)
476
            if compgenargs:
477
              sw.Write("compgenargs=%s",
478
                       utils.ShellQuote(" ".join(compgenargs)))
479
          finally:
480
            sw.DecIndent()
481

    
482
          wrote_arg = True
483

    
484
      if wrote_arg:
485
        sw.Write("fi")
486

    
487
    if self.args:
488
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
489
    else:
490
      # $compgenargs exists only if there are arguments
491
      WriteCompReply(sw, '-W "$choices"')
492

    
493
  def WriteTo(self, sw):
494
    self._FindFirstArgument(sw)
495
    self._CompleteOptionValues(sw)
496
    self._CompleteArguments(sw)
497

    
498

    
499
def WriteCompletion(sw, scriptname, funcname,
500
                    commands=None,
501
                    opts=None, args=None):
502
  """Writes the completion code for one command.
503

    
504
  @type sw: ShellWriter
505
  @param sw: Script writer
506
  @type scriptname: string
507
  @param scriptname: Name of command line program
508
  @type funcname: string
509
  @param funcname: Shell function name
510
  @type commands: list
511
  @param commands: List of all subcommands in this program
512

    
513
  """
514
  sw.Write("%s() {", funcname)
515
  sw.IncIndent()
516
  try:
517
    sw.Write("local "
518
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
519
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
520
             ' i first_arg_idx choices compgenargs arg_idx optcur')
521

    
522
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
523
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
524
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
525

    
526
    sw.Write("COMPREPLY=()")
527

    
528
    if opts is not None and args is not None:
529
      assert not commands
530
      CompletionWriter(0, opts, args).WriteTo(sw)
531

    
532
    else:
533
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
534
      sw.IncIndent()
535
      try:
536
        # Complete the command name
537
        WriteCompReply(sw,
538
                       ("-W %s" %
539
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
540
      finally:
541
        sw.DecIndent()
542
      sw.Write("fi")
543

    
544
      # We're doing options and arguments to commands
545
      sw.Write("""case "${COMP_WORDS[1]}" in""")
546
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
547
        if not (argdef or optdef):
548
          continue
549

    
550
        # TODO: Group by arguments and options
551
        sw.Write("%s)", utils.ShellQuote(cmd))
552
        sw.IncIndent()
553
        try:
554
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
555
        finally:
556
          sw.DecIndent()
557

    
558
        sw.Write(";;")
559
      sw.Write("esac")
560
  finally:
561
    sw.DecIndent()
562
  sw.Write("}")
563

    
564
  sw.Write("complete -F %s -o filenames %s",
565
           utils.ShellQuote(funcname),
566
           utils.ShellQuote(scriptname))
567

    
568

    
569
def GetFunctionName(name):
570
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
571

    
572

    
573
def GetCommands(filename, module):
574
  """Returns the commands defined in a module.
575

    
576
  Aliases are also added as commands.
577

    
578
  """
579
  try:
580
    commands = getattr(module, "commands")
581
  except AttributeError:
582
    raise Exception("Script %s doesn't have 'commands' attribute" %
583
                    filename)
584

    
585
  # Add the implicit "--help" option
586
  help_option = cli.cli_option("-h", "--help", default=False,
587
                               action="store_true")
588

    
589
  for name, (_, _, optdef, _, _) in commands.items():
590
    if help_option not in optdef:
591
      optdef.append(help_option)
592
    for opt in cli.COMMON_OPTS:
593
      if opt in optdef:
594
        raise Exception("Common option '%s' listed for command '%s' in %s" %
595
                        (opt, name, filename))
596
      optdef.append(opt)
597

    
598
  # Use aliases
599
  aliases = getattr(module, "aliases", {})
600
  if aliases:
601
    commands = commands.copy()
602
    for name, target in aliases.iteritems():
603
      commands[name] = commands[target]
604

    
605
  return commands
606

    
607

    
608
def main():
609
  buf = StringIO()
610
  sw = utils.ShellWriter(buf)
611

    
612
  WritePreamble(sw)
613

    
614
  # gnt-* scripts
615
  for scriptname in _autoconf.GNT_SCRIPTS:
616
    filename = "scripts/%s" % scriptname
617

    
618
    WriteCompletion(sw, scriptname,
619
                    GetFunctionName(scriptname),
620
                    commands=GetCommands(filename,
621
                                         build.LoadModule(filename)))
622

    
623
  # Burnin script
624
  burnin = build.LoadModule("tools/burnin")
625
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
626
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
627

    
628
  print buf.getvalue()
629

    
630

    
631
if __name__ == "__main__":
632
  main()