Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ f5ce7613

History | View | Annotate | Download (18.4 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2009, 2010, 2011, 2012 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
import itertools
32
from cStringIO import StringIO
33

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

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

    
43
#: Regular expression describing desired format of option names. Long names can
44
#: contain lowercase characters, numbers and dashes only.
45
_OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
46

    
47

    
48
def WritePreamble(sw):
49
  """Writes the script preamble.
50

    
51
  Helper functions should be written here.
52

    
53
  """
54
  sw.Write("# This script is automatically generated at build time.")
55
  sw.Write("# Do not modify manually.")
56

    
57
  sw.Write("_gnt_log() {")
58
  sw.IncIndent()
59
  try:
60
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
61
    sw.IncIndent()
62
    try:
63
      sw.Write("{")
64
      sw.IncIndent()
65
      try:
66
        sw.Write("echo ---")
67
        sw.Write("echo \"$@\"")
68
        sw.Write("echo")
69
      finally:
70
        sw.DecIndent()
71
      sw.Write("} >> $GANETI_COMPL_LOG")
72
    finally:
73
      sw.DecIndent()
74
    sw.Write("fi")
75
  finally:
76
    sw.DecIndent()
77
  sw.Write("}")
78

    
79
  sw.Write("_ganeti_nodes() {")
80
  sw.IncIndent()
81
  try:
82
    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
83
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
84
  finally:
85
    sw.DecIndent()
86
  sw.Write("}")
87

    
88
  sw.Write("_ganeti_instances() {")
89
  sw.IncIndent()
90
  try:
91
    instance_list_path = os.path.join(constants.DATA_DIR,
92
                                      "ssconf_instance_list")
93
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
94
  finally:
95
    sw.DecIndent()
96
  sw.Write("}")
97

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

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

    
125
  sw.Write("_ganeti_nodegroup() {")
126
  sw.IncIndent()
127
  try:
128
    nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
129
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
130
  finally:
131
    sw.DecIndent()
132
  sw.Write("}")
133

    
134
  # Params: <offset> <options with values> <options without values>
135
  # Result variable: $first_arg_idx
136
  sw.Write("_ganeti_find_first_arg() {")
137
  sw.IncIndent()
138
  try:
139
    sw.Write("local w i")
140

    
141
    sw.Write("first_arg_idx=")
142
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
143
    sw.IncIndent()
144
    try:
145
      sw.Write("w=${COMP_WORDS[$i]}")
146

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

    
150
      # Skip
151
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
152

    
153
      # Ah, we found the first argument
154
      sw.Write("else first_arg_idx=$i; break;")
155
      sw.Write("fi")
156
    finally:
157
      sw.DecIndent()
158
    sw.Write("done")
159
  finally:
160
    sw.DecIndent()
161
  sw.Write("}")
162

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

    
186
    # Calculate position of current argument
187
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
188
    sw.Write("choices=")
189
  finally:
190
    sw.DecIndent()
191
  sw.Write("}")
192

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

    
214
    sw.Write("_gnt_log optcur=\"'$optcur'\"")
215

    
216
    sw.Write("return 1")
217
  finally:
218
    sw.DecIndent()
219
  sw.Write("}")
220

    
221
  # Params: <compgen options>
222
  # Result variable: $COMPREPLY
223
  sw.Write("_gnt_compgen() {")
224
  sw.IncIndent()
225
  try:
226
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
227
    sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
228
  finally:
229
    sw.DecIndent()
230
  sw.Write("}")
231

    
232

    
233
def WriteCompReply(sw, args, cur="\"$cur\""):
234
  sw.Write("_gnt_compgen %s -- %s", args, cur)
235
  sw.Write("return")
236

    
237

    
238
class CompletionWriter:
239
  """Command completion writer class.
240

    
241
  """
242
  def __init__(self, arg_offset, opts, args):
243
    self.arg_offset = arg_offset
244
    self.opts = opts
245
    self.args = args
246

    
247
    for opt in opts:
248
      # While documented, these variables aren't seen as public attributes by
249
      # pylint. pylint: disable=W0212
250
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
251

    
252
      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
253
      if invalid:
254
        raise Exception("Option names don't match regular expression '%s': %s" %
255
                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
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
      # our custom option type
298
      if opt.type == "bool":
299
        suggest = ["yes", "no"]
300

    
301
      if not suggest:
302
        suggest = opt.choices
303

    
304
      if (isinstance(suggest, (int, long)) and
305
          suggest in cli.OPT_COMPL_ALL):
306
        key = suggest
307
      elif suggest:
308
        key = " ".join(sorted(suggest))
309
      else:
310
        key = ""
311

    
312
      values.setdefault(key, []).extend(opt.all_names)
313

    
314
    # Don't write any code if there are no option values
315
    if not values:
316
      return
317

    
318
    cur = "\"$optcur\""
319

    
320
    wrote_opt = False
321

    
322
    for (suggest, allnames) in values.items():
323
      longnames = [i for i in allnames if i.startswith("--")]
324

    
325
      if wrote_opt:
326
        condcmd = "elif"
327
      else:
328
        condcmd = "if"
329

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

    
351
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
352
          sw.IncIndent()
353
          try:
354
            sw.Write("node1=\"${optcur%%:*}\"")
355

    
356
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
357
            sw.IncIndent()
358
            try:
359
              sw.Write("pfx=\"$node1:\"")
360
            finally:
361
              sw.DecIndent()
362
            sw.Write("fi")
363
          finally:
364
            sw.DecIndent()
365
          sw.Write("fi")
366

    
367
          sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
368
                   " node1=\"'$node1'\"")
369

    
370
          sw.Write("for i in $(_ganeti_nodes); do")
371
          sw.IncIndent()
372
          try:
373
            sw.Write("if [[ -z \"$node1\" ]]; then")
374
            sw.IncIndent()
375
            try:
376
              sw.Write("tmp=\"$tmp $i $i:\"")
377
            finally:
378
              sw.DecIndent()
379
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
380
            sw.IncIndent()
381
            try:
382
              sw.Write("tmp=\"$tmp $i\"")
383
            finally:
384
              sw.DecIndent()
385
            sw.Write("fi")
386
          finally:
387
            sw.DecIndent()
388
          sw.Write("done")
389

    
390
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
391
        else:
392
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
393
      finally:
394
        sw.DecIndent()
395

    
396
      wrote_opt = True
397

    
398
    if wrote_opt:
399
      sw.Write("fi")
400

    
401
    return
402

    
403
  def _CompleteArguments(self, sw):
404
    if not (self.opts or self.args):
405
      return
406

    
407
    all_option_names = []
408
    for opt in self.opts:
409
      all_option_names.extend(opt.all_names)
410
    all_option_names.sort()
411

    
412
    # List options if no argument has been specified yet
413
    sw.Write("_ganeti_list_options %s",
414
             utils.ShellQuote(" ".join(all_option_names)))
415

    
416
    if self.args:
417
      last_idx = len(self.args) - 1
418
      last_arg_end = 0
419
      varlen_arg_idx = None
420
      wrote_arg = False
421

    
422
      sw.Write("compgenargs=")
423

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

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

    
433
        compgenargs = []
434

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

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

    
471
        last_arg_end += arg.min
472

    
473
        if choices or compgenargs:
474
          if wrote_arg:
475
            condcmd = "elif"
476
          else:
477
            condcmd = "if"
478

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

    
490
          wrote_arg = True
491

    
492
      if wrote_arg:
493
        sw.Write("fi")
494

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

    
501
  def WriteTo(self, sw):
502
    self._FindFirstArgument(sw)
503
    self._CompleteOptionValues(sw)
504
    self._CompleteArguments(sw)
505

    
506

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

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

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

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

    
534
    sw.Write("COMPREPLY=()")
535

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

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

    
552
      # Group commands by arguments and options
553
      grouped_cmds = {}
554
      for cmd, (_, argdef, optdef, _, _) in commands.items():
555
        if not (argdef or optdef):
556
          continue
557
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
558

    
559
      # We're doing options and arguments to commands
560
      sw.Write("""case "${COMP_WORDS[1]}" in""")
561
      sort_grouped = sorted(grouped_cmds.items(),
562
                            key=lambda (_, y): sorted(y)[0])
563
      for ((argdef, optdef), cmds) in sort_grouped:
564
        assert argdef or optdef
565
        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
566
        sw.IncIndent()
567
        try:
568
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
569
        finally:
570
          sw.DecIndent()
571
        sw.Write(";;")
572
      sw.Write("esac")
573
  finally:
574
    sw.DecIndent()
575
  sw.Write("}")
576

    
577
  sw.Write("complete -F %s -o filenames %s",
578
           utils.ShellQuote(funcname),
579
           utils.ShellQuote(scriptname))
580

    
581

    
582
def GetFunctionName(name):
583
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
584

    
585

    
586
def GetCommands(filename, module):
587
  """Returns the commands defined in a module.
588

    
589
  Aliases are also added as commands.
590

    
591
  """
592
  try:
593
    commands = getattr(module, "commands")
594
  except AttributeError:
595
    raise Exception("Script %s doesn't have 'commands' attribute" %
596
                    filename)
597

    
598
  # Add the implicit "--help" option
599
  help_option = cli.cli_option("-h", "--help", default=False,
600
                               action="store_true")
601

    
602
  for name, (_, _, optdef, _, _) in commands.items():
603
    if help_option not in optdef:
604
      optdef.append(help_option)
605
    for opt in cli.COMMON_OPTS:
606
      if opt in optdef:
607
        raise Exception("Common option '%s' listed for command '%s' in %s" %
608
                        (opt, name, filename))
609
      optdef.append(opt)
610

    
611
  # Use aliases
612
  aliases = getattr(module, "aliases", {})
613
  if aliases:
614
    commands = commands.copy()
615
    for name, target in aliases.items():
616
      commands[name] = commands[target]
617

    
618
  return commands
619

    
620

    
621
def main():
622
  buf = StringIO()
623
  sw = utils.ShellWriter(buf)
624

    
625
  WritePreamble(sw)
626

    
627
  # gnt-* scripts
628
  for scriptname in _autoconf.GNT_SCRIPTS:
629
    filename = "scripts/%s" % scriptname
630

    
631
    WriteCompletion(sw, scriptname,
632
                    GetFunctionName(scriptname),
633
                    commands=GetCommands(filename,
634
                                         build.LoadModule(filename)))
635

    
636
  # Burnin script
637
  burnin = build.LoadModule("tools/burnin")
638
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
639
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
640

    
641
  print buf.getvalue()
642

    
643

    
644
if __name__ == "__main__":
645
  main()