Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ b459a848

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=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=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.ArgGroup):
438
          choices = "$(_ganeti_nodegroup)"
439
        elif isinstance(arg, cli.ArgJobId):
440
          choices = "$(_ganeti_jobs)"
441
        elif isinstance(arg, cli.ArgOs):
442
          choices = "$(_ganeti_os)"
443
        elif isinstance(arg, cli.ArgFile):
444
          choices = ""
445
          compgenargs.append("-f")
446
        elif isinstance(arg, cli.ArgCommand):
447
          choices = ""
448
          compgenargs.append("-c")
449
        elif isinstance(arg, cli.ArgHost):
450
          choices = ""
451
          compgenargs.append("-A hostname")
452
        else:
453
          raise Exception("Unknown argument type %r" % arg)
454

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

    
465
        last_arg_end += arg.min
466

    
467
        if choices or compgenargs:
468
          if wrote_arg:
469
            condcmd = "elif"
470
          else:
471
            condcmd = "if"
472

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

    
484
          wrote_arg = True
485

    
486
      if wrote_arg:
487
        sw.Write("fi")
488

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

    
495
  def WriteTo(self, sw):
496
    self._FindFirstArgument(sw)
497
    self._CompleteOptionValues(sw)
498
    self._CompleteArguments(sw)
499

    
500

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

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

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

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

    
528
    sw.Write("COMPREPLY=()")
529

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

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

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

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

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

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

    
570

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

    
574

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

    
578
  Aliases are also added as commands.
579

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

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

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

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

    
607
  return commands
608

    
609

    
610
def main():
611
  buf = StringIO()
612
  sw = utils.ShellWriter(buf)
613

    
614
  WritePreamble(sw)
615

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

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

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

    
630
  print buf.getvalue()
631

    
632

    
633
if __name__ == "__main__":
634
  main()