Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ b33cad4a

History | View | Annotate | Download (18.4 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
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("_ganeti_dbglog() {")
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("_ganeti_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("_ganeti_dbglog 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("_ganeti_compgen() {")
224
  sw.IncIndent()
225
  try:
226
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
227
    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
228
  finally:
229
    sw.DecIndent()
230
  sw.Write("}")
231

    
232

    
233
def WriteCompReply(sw, args, cur="\"$cur\""):
234
  sw.Write("_ganeti_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-msg=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.iteritems():
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 _ganeti_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("_ganeti_dbglog 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
      # Write some debug comments
423
      for idx, arg in enumerate(self.args):
424
        sw.Write("# %s: %r", idx, arg)
425

    
426
      sw.Write("compgenargs=")
427

    
428
      for idx, arg in enumerate(self.args):
429
        assert arg.min is not None and arg.min >= 0
430
        assert not (idx < last_idx and arg.max is None)
431

    
432
        if arg.min != arg.max or arg.max is None:
433
          if varlen_arg_idx is not None:
434
            raise Exception("Only one argument can have a variable length")
435
          varlen_arg_idx = idx
436

    
437
        compgenargs = []
438

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

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

    
475
        last_arg_end += arg.min
476

    
477
        if choices or compgenargs:
478
          if wrote_arg:
479
            condcmd = "elif"
480
          else:
481
            condcmd = "if"
482

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

    
494
          wrote_arg = True
495

    
496
      if wrote_arg:
497
        sw.Write("fi")
498

    
499
    if self.args:
500
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
501
    else:
502
      # $compgenargs exists only if there are arguments
503
      WriteCompReply(sw, '-W "$choices"')
504

    
505
  def WriteTo(self, sw):
506
    self._FindFirstArgument(sw)
507
    self._CompleteOptionValues(sw)
508
    self._CompleteArguments(sw)
509

    
510

    
511
def WriteCompletion(sw, scriptname, funcname,
512
                    commands=None,
513
                    opts=None, args=None):
514
  """Writes the completion code for one command.
515

    
516
  @type sw: ShellWriter
517
  @param sw: Script writer
518
  @type scriptname: string
519
  @param scriptname: Name of command line program
520
  @type funcname: string
521
  @param funcname: Shell function name
522
  @type commands: list
523
  @param commands: List of all subcommands in this program
524

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

    
534
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
535
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
536
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
537

    
538
    sw.Write("COMPREPLY=()")
539

    
540
    if opts is not None and args is not None:
541
      assert not commands
542
      CompletionWriter(0, opts, args).WriteTo(sw)
543

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

    
556
      # Group commands by arguments and options
557
      grouped_cmds = {}
558
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
559
        if not (argdef or optdef):
560
          continue
561
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
562

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

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

    
583

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

    
587

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

    
591
  Aliases are also added as commands.
592

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

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

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

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

    
620
  return commands
621

    
622

    
623
def main():
624
  buf = StringIO()
625
  sw = utils.ShellWriter(buf)
626

    
627
  WritePreamble(sw)
628

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

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

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

    
643
  print buf.getvalue()
644

    
645

    
646
if __name__ == "__main__":
647
  main()