Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 13718ded

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
from ganeti import pathutils
39

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

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

    
48

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

    
52
  Helper functions should be written here.
53

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
233

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

    
238

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

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

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

    
253
      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
254
      if invalid:
255
        raise Exception("Option names don't match regular expression '%s': %s" %
256
                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
257

    
258
  def _FindFirstArgument(self, sw):
259
    ignore = []
260
    skip_one = []
261

    
262
    for opt in self.opts:
263
      if opt.takes_value():
264
        # Ignore value
265
        for i in opt.all_names:
266
          if i.startswith("--"):
267
            ignore.append("%s=*" % utils.ShellQuote(i))
268
          skip_one.append(utils.ShellQuote(i))
269
      else:
270
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
271

    
272
    ignore = sorted(utils.UniqueSequence(ignore))
273
    skip_one = sorted(utils.UniqueSequence(skip_one))
274

    
275
    if ignore or skip_one:
276
      # Try to locate first argument
277
      sw.Write("_ganeti_find_first_arg %s %s %s",
278
               self.arg_offset + 1,
279
               utils.ShellQuote("|".join(skip_one)),
280
               utils.ShellQuote("|".join(ignore)))
281
    else:
282
      # When there are no options the first argument is always at position
283
      # offset + 1
284
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
285

    
286
  def _CompleteOptionValues(self, sw):
287
    # Group by values
288
    # "values" -> [optname1, optname2, ...]
289
    values = {}
290

    
291
    for opt in self.opts:
292
      if not opt.takes_value():
293
        continue
294

    
295
      # Only static choices implemented so far (e.g. no node list)
296
      suggest = getattr(opt, "completion_suggest", None)
297

    
298
      # our custom option type
299
      if opt.type == "bool":
300
        suggest = ["yes", "no"]
301

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

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

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

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

    
319
    cur = "\"$optcur\""
320

    
321
    wrote_opt = False
322

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

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

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

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

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

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

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

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

    
397
      wrote_opt = True
398

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

    
402
    return
403

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

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

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

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

    
423
      sw.Write("compgenargs=")
424

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

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

    
434
        compgenargs = []
435

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

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

    
472
        last_arg_end += arg.min
473

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

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

    
491
          wrote_arg = True
492

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

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

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

    
507

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

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

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

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

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

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

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

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

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

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

    
582

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

    
586

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

    
590
  Aliases are also added as commands.
591

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

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

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

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

    
619
  return commands
620

    
621

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

    
626
  WritePreamble(sw)
627

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

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

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

    
642
  print buf.getvalue()
643

    
644

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