Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ eb980c5a

History | View | Annotate | Download (17.9 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_PARAMS):
296
        # We do not provide parameter completion yet
297
        continue
298
      elif (isinstance(suggest, (int, long)) and
299
          suggest in cli.OPT_COMPL_ALL):
300
        key = suggest
301
      elif suggest:
302
        key = " ".join(sorted(suggest))
303
      else:
304
        key = ""
305

    
306
      values.setdefault(key, []).extend(opt.all_names)
307

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

    
312
    cur = "\"$optcur\""
313

    
314
    wrote_opt = False
315

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

    
319
      if wrote_opt:
320
        condcmd = "elif"
321
      else:
322
        condcmd = "if"
323

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

    
345
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
346
          sw.IncIndent()
347
          try:
348
            sw.Write("node1=\"${optcur%%:*}\"")
349

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

    
361
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
362
                   " node1=\"'$node1'\"")
363

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

    
384
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
385
        else:
386
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
387
      finally:
388
        sw.DecIndent()
389

    
390
      wrote_opt = True
391

    
392
    if wrote_opt:
393
      sw.Write("fi")
394

    
395
    return
396

    
397
  def _CompleteArguments(self, sw):
398
    if not (self.opts or self.args):
399
      return
400

    
401
    all_option_names = []
402
    for opt in self.opts:
403
      all_option_names.extend(opt.all_names)
404
    all_option_names.sort()
405

    
406
    # List options if no argument has been specified yet
407
    sw.Write("_ganeti_list_options %s",
408
             utils.ShellQuote(" ".join(all_option_names)))
409

    
410
    if self.args:
411
      last_idx = len(self.args) - 1
412
      last_arg_end = 0
413
      varlen_arg_idx = None
414
      wrote_arg = False
415

    
416
      # Write some debug comments
417
      for idx, arg in enumerate(self.args):
418
        sw.Write("# %s: %r", idx, arg)
419

    
420
      sw.Write("compgenargs=")
421

    
422
      for idx, arg in enumerate(self.args):
423
        assert arg.min is not None and arg.min >= 0
424
        assert not (idx < last_idx and arg.max is None)
425

    
426
        if arg.min != arg.max or arg.max is None:
427
          if varlen_arg_idx is not None:
428
            raise Exception("Only one argument can have a variable length")
429
          varlen_arg_idx = idx
430

    
431
        compgenargs = []
432

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

    
459
        if arg.min == 1 and arg.max == 1:
460
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
461
        elif arg.max is None:
462
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
463
        elif arg.min <= arg.max:
464
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
465
                     (last_arg_end, last_arg_end + arg.max))
466
        else:
467
          raise Exception("Unable to generate argument position condition")
468

    
469
        last_arg_end += arg.min
470

    
471
        if choices or compgenargs:
472
          if wrote_arg:
473
            condcmd = "elif"
474
          else:
475
            condcmd = "if"
476

    
477
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
478
          sw.IncIndent()
479
          try:
480
            if choices:
481
              sw.Write("""choices="$choices "%s""", choices)
482
            if compgenargs:
483
              sw.Write("compgenargs=%s",
484
                       utils.ShellQuote(" ".join(compgenargs)))
485
          finally:
486
            sw.DecIndent()
487

    
488
          wrote_arg = True
489

    
490
      if wrote_arg:
491
        sw.Write("fi")
492

    
493
    if self.args:
494
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
495
    else:
496
      # $compgenargs exists only if there are arguments
497
      WriteCompReply(sw, '-W "$choices"')
498

    
499
  def WriteTo(self, sw):
500
    self._FindFirstArgument(sw)
501
    self._CompleteOptionValues(sw)
502
    self._CompleteArguments(sw)
503

    
504

    
505
def WriteCompletion(sw, scriptname, funcname,
506
                    commands=None,
507
                    opts=None, args=None):
508
  """Writes the completion code for one command.
509

    
510
  @type sw: ShellWriter
511
  @param sw: Script writer
512
  @type scriptname: string
513
  @param scriptname: Name of command line program
514
  @type funcname: string
515
  @param funcname: Shell function name
516
  @type commands: list
517
  @param commands: List of all subcommands in this program
518

    
519
  """
520
  sw.Write("%s() {", funcname)
521
  sw.IncIndent()
522
  try:
523
    sw.Write("local "
524
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
525
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
526
             ' i first_arg_idx choices compgenargs arg_idx optcur')
527

    
528
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
529
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
530
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
531

    
532
    sw.Write("COMPREPLY=()")
533

    
534
    if opts is not None and args is not None:
535
      assert not commands
536
      CompletionWriter(0, opts, args).WriteTo(sw)
537

    
538
    else:
539
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
540
      sw.IncIndent()
541
      try:
542
        # Complete the command name
543
        WriteCompReply(sw,
544
                       ("-W %s" %
545
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
546
      finally:
547
        sw.DecIndent()
548
      sw.Write("fi")
549

    
550
      # We're doing options and arguments to commands
551
      sw.Write("""case "${COMP_WORDS[1]}" in""")
552
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
553
        if not (argdef or optdef):
554
          continue
555

    
556
        # TODO: Group by arguments and options
557
        sw.Write("%s)", utils.ShellQuote(cmd))
558
        sw.IncIndent()
559
        try:
560
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
561
        finally:
562
          sw.DecIndent()
563

    
564
        sw.Write(";;")
565
      sw.Write("esac")
566
  finally:
567
    sw.DecIndent()
568
  sw.Write("}")
569

    
570
  sw.Write("complete -F %s -o filenames %s",
571
           utils.ShellQuote(funcname),
572
           utils.ShellQuote(scriptname))
573

    
574

    
575
def GetFunctionName(name):
576
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
577

    
578

    
579
def GetCommands(filename, module):
580
  """Returns the commands defined in a module.
581

    
582
  Aliases are also added as commands.
583

    
584
  """
585
  try:
586
    commands = getattr(module, "commands")
587
  except AttributeError:
588
    raise Exception("Script %s doesn't have 'commands' attribute" %
589
                    filename)
590

    
591
  # Add the implicit "--help" option
592
  help_option = cli.cli_option("-h", "--help", default=False,
593
                               action="store_true")
594

    
595
  for name, (_, _, optdef, _, _) in commands.items():
596
    if help_option not in optdef:
597
      optdef.append(help_option)
598
    for opt in cli.COMMON_OPTS:
599
      if opt in optdef:
600
        raise Exception("Common option '%s' listed for command '%s' in %s" %
601
                        (opt, name, filename))
602
      optdef.append(opt)
603

    
604
  # Use aliases
605
  aliases = getattr(module, "aliases", {})
606
  if aliases:
607
    commands = commands.copy()
608
    for name, target in aliases.iteritems():
609
      commands[name] = commands[target]
610

    
611
  return commands
612

    
613

    
614
def main():
615
  buf = StringIO()
616
  sw = utils.ShellWriter(buf)
617

    
618
  WritePreamble(sw)
619

    
620
  # gnt-* scripts
621
  for scriptname in _autoconf.GNT_SCRIPTS:
622
    filename = "scripts/%s" % scriptname
623

    
624
    WriteCompletion(sw, scriptname,
625
                    GetFunctionName(scriptname),
626
                    commands=GetCommands(filename,
627
                                         build.LoadModule(filename)))
628

    
629
  # Burnin script
630
  burnin = build.LoadModule("tools/burnin")
631
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
632
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
633

    
634
  print buf.getvalue()
635

    
636

    
637
if __name__ == "__main__":
638
  main()