Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ e7b61bb0

History | View | Annotate | Download (17.8 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
class ShellWriter:
44
  """Helper class to write scripts with indentation.
45

    
46
  """
47
  INDENT_STR = "  "
48

    
49
  def __init__(self, fh):
50
    self._fh = fh
51
    self._indent = 0
52

    
53
  def IncIndent(self):
54
    """Increase indentation level by 1.
55

    
56
    """
57
    self._indent += 1
58

    
59
  def DecIndent(self):
60
    """Decrease indentation level by 1.
61

    
62
    """
63
    assert self._indent > 0
64
    self._indent -= 1
65

    
66
  def Write(self, txt, *args):
67
    """Write line to output file.
68

    
69
    """
70
    self._fh.write(self._indent * self.INDENT_STR)
71

    
72
    if args:
73
      self._fh.write(txt % args)
74
    else:
75
      self._fh.write(txt)
76

    
77
    self._fh.write("\n")
78

    
79

    
80
def WritePreamble(sw):
81
  """Writes the script preamble.
82

    
83
  Helper functions should be written here.
84

    
85
  """
86
  sw.Write("# This script is automatically generated at build time.")
87
  sw.Write("# Do not modify manually.")
88

    
89
  sw.Write("_ganeti_dbglog() {")
90
  sw.IncIndent()
91
  try:
92
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
93
    sw.IncIndent()
94
    try:
95
      sw.Write("{")
96
      sw.IncIndent()
97
      try:
98
        sw.Write("echo ---")
99
        sw.Write("echo \"$@\"")
100
        sw.Write("echo")
101
      finally:
102
        sw.DecIndent()
103
      sw.Write("} >> $GANETI_COMPL_LOG")
104
    finally:
105
      sw.DecIndent()
106
    sw.Write("fi")
107
  finally:
108
    sw.DecIndent()
109
  sw.Write("}")
110

    
111
  sw.Write("_ganeti_nodes() {")
112
  sw.IncIndent()
113
  try:
114
    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
115
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
116
  finally:
117
    sw.DecIndent()
118
  sw.Write("}")
119

    
120
  sw.Write("_ganeti_instances() {")
121
  sw.IncIndent()
122
  try:
123
    instance_list_path = os.path.join(constants.DATA_DIR,
124
                                      "ssconf_instance_list")
125
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
126
  finally:
127
    sw.DecIndent()
128
  sw.Write("}")
129

    
130
  sw.Write("_ganeti_jobs() {")
131
  sw.IncIndent()
132
  try:
133
    # FIXME: this is really going into the internals of the job queue
134
    sw.Write(("local jlist=$( shopt -s nullglob &&"
135
              " cd %s 2>/dev/null && echo job-* || : )"),
136
             utils.ShellQuote(constants.QUEUE_DIR))
137
    sw.Write('echo "${jlist//job-/}"')
138
  finally:
139
    sw.DecIndent()
140
  sw.Write("}")
141

    
142
  for (fnname, paths) in [
143
      ("os", constants.OS_SEARCH_PATH),
144
      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
145
      ]:
146
    sw.Write("_ganeti_%s() {", fnname)
147
    sw.IncIndent()
148
    try:
149
      # FIXME: Make querying the master for all OSes cheap
150
      for path in paths:
151
        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
152
                 utils.ShellQuote(path))
153
    finally:
154
      sw.DecIndent()
155
    sw.Write("}")
156

    
157
  # Params: <offset> <options with values> <options without values>
158
  # Result variable: $first_arg_idx
159
  sw.Write("_ganeti_find_first_arg() {")
160
  sw.IncIndent()
161
  try:
162
    sw.Write("local w i")
163

    
164
    sw.Write("first_arg_idx=")
165
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
166
    sw.IncIndent()
167
    try:
168
      sw.Write("w=${COMP_WORDS[$i]}")
169

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

    
173
      # Skip
174
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
175

    
176
      # Ah, we found the first argument
177
      sw.Write("else first_arg_idx=$i; break;")
178
      sw.Write("fi")
179
    finally:
180
      sw.DecIndent()
181
    sw.Write("done")
182
  finally:
183
    sw.DecIndent()
184
  sw.Write("}")
185

    
186
  # Params: <list of options separated by space>
187
  # Input variable: $first_arg_idx
188
  # Result variables: $arg_idx, $choices
189
  sw.Write("_ganeti_list_options() {")
190
  sw.IncIndent()
191
  try:
192
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
193
    sw.IncIndent()
194
    try:
195
      sw.Write("arg_idx=0")
196
      # Show options only if the current word starts with a dash
197
      sw.Write("""if [[ "$cur" == -* ]]; then""")
198
      sw.IncIndent()
199
      try:
200
        sw.Write("choices=$1")
201
      finally:
202
        sw.DecIndent()
203
      sw.Write("fi")
204
      sw.Write("return")
205
    finally:
206
      sw.DecIndent()
207
    sw.Write("fi")
208

    
209
    # Calculate position of current argument
210
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
211
    sw.Write("choices=")
212
  finally:
213
    sw.DecIndent()
214
  sw.Write("}")
215

    
216
  # Params: <long options with equal sign> <all options>
217
  # Result variable: $optcur
218
  sw.Write("_ganeti_checkopt() {")
219
  sw.IncIndent()
220
  try:
221
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
222
    sw.IncIndent()
223
    try:
224
      sw.Write("optcur=\"${cur#--*=}\"")
225
      sw.Write("return 0")
226
    finally:
227
      sw.DecIndent()
228
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
229
    sw.IncIndent()
230
    try:
231
      sw.Write("optcur=\"$cur\"")
232
      sw.Write("return 0")
233
    finally:
234
      sw.DecIndent()
235
    sw.Write("fi")
236

    
237
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
238

    
239
    sw.Write("return 1")
240
  finally:
241
    sw.DecIndent()
242
  sw.Write("}")
243

    
244
  # Params: <compgen options>
245
  # Result variable: $COMPREPLY
246
  sw.Write("_ganeti_compgen() {")
247
  sw.IncIndent()
248
  try:
249
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
250
    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
251
  finally:
252
    sw.DecIndent()
253
  sw.Write("}")
254

    
255

    
256
def WriteCompReply(sw, args, cur="\"$cur\""):
257
  sw.Write("_ganeti_compgen %s -- %s", args, cur)
258
  sw.Write("return")
259

    
260

    
261
class CompletionWriter:
262
  """Command completion writer class.
263

    
264
  """
265
  def __init__(self, arg_offset, opts, args):
266
    self.arg_offset = arg_offset
267
    self.opts = opts
268
    self.args = args
269

    
270
    for opt in opts:
271
      # While documented, these variables aren't seen as public attributes by
272
      # pylint. pylint: disable-msg=W0212
273
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
274

    
275
  def _FindFirstArgument(self, sw):
276
    ignore = []
277
    skip_one = []
278

    
279
    for opt in self.opts:
280
      if opt.takes_value():
281
        # Ignore value
282
        for i in opt.all_names:
283
          if i.startswith("--"):
284
            ignore.append("%s=*" % utils.ShellQuote(i))
285
          skip_one.append(utils.ShellQuote(i))
286
      else:
287
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
288

    
289
    ignore = sorted(utils.UniqueSequence(ignore))
290
    skip_one = sorted(utils.UniqueSequence(skip_one))
291

    
292
    if ignore or skip_one:
293
      # Try to locate first argument
294
      sw.Write("_ganeti_find_first_arg %s %s %s",
295
               self.arg_offset + 1,
296
               utils.ShellQuote("|".join(skip_one)),
297
               utils.ShellQuote("|".join(ignore)))
298
    else:
299
      # When there are no options the first argument is always at position
300
      # offset + 1
301
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
302

    
303
  def _CompleteOptionValues(self, sw):
304
    # Group by values
305
    # "values" -> [optname1, optname2, ...]
306
    values = {}
307

    
308
    for opt in self.opts:
309
      if not opt.takes_value():
310
        continue
311

    
312
      # Only static choices implemented so far (e.g. no node list)
313
      suggest = getattr(opt, "completion_suggest", None)
314

    
315
      # our custom option type
316
      if opt.type == "bool":
317
        suggest = ["yes", "no"]
318

    
319
      if not suggest:
320
        suggest = opt.choices
321

    
322
      if (isinstance(suggest, (int, long)) and
323
          suggest in cli.OPT_COMPL_ALL):
324
        key = suggest
325
      elif suggest:
326
        key = " ".join(sorted(suggest))
327
      else:
328
        key = ""
329

    
330
      values.setdefault(key, []).extend(opt.all_names)
331

    
332
    # Don't write any code if there are no option values
333
    if not values:
334
      return
335

    
336
    cur = "\"$optcur\""
337

    
338
    wrote_opt = False
339

    
340
    for (suggest, allnames) in values.iteritems():
341
      longnames = [i for i in allnames if i.startswith("--")]
342

    
343
      if wrote_opt:
344
        condcmd = "elif"
345
      else:
346
        condcmd = "if"
347

    
348
      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
349
               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
350
               utils.ShellQuote("|".join(allnames)))
351
      sw.IncIndent()
352
      try:
353
        if suggest == cli.OPT_COMPL_MANY_NODES:
354
          # TODO: Implement comma-separated values
355
          WriteCompReply(sw, "-W ''", cur=cur)
356
        elif suggest == cli.OPT_COMPL_ONE_NODE:
357
          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
358
        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
359
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
360
        elif suggest == cli.OPT_COMPL_ONE_OS:
361
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
362
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
363
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
364
        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
365
          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
366

    
367
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
368
          sw.IncIndent()
369
          try:
370
            sw.Write("node1=\"${optcur%%:*}\"")
371

    
372
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
373
            sw.IncIndent()
374
            try:
375
              sw.Write("pfx=\"$node1:\"")
376
            finally:
377
              sw.DecIndent()
378
            sw.Write("fi")
379
          finally:
380
            sw.DecIndent()
381
          sw.Write("fi")
382

    
383
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
384
                   " node1=\"'$node1'\"")
385

    
386
          sw.Write("for i in $(_ganeti_nodes); do")
387
          sw.IncIndent()
388
          try:
389
            sw.Write("if [[ -z \"$node1\" ]]; then")
390
            sw.IncIndent()
391
            try:
392
              sw.Write("tmp=\"$tmp $i $i:\"")
393
            finally:
394
              sw.DecIndent()
395
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
396
            sw.IncIndent()
397
            try:
398
              sw.Write("tmp=\"$tmp $i\"")
399
            finally:
400
              sw.DecIndent()
401
            sw.Write("fi")
402
          finally:
403
            sw.DecIndent()
404
          sw.Write("done")
405

    
406
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
407
        else:
408
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
409
      finally:
410
        sw.DecIndent()
411

    
412
      wrote_opt = True
413

    
414
    if wrote_opt:
415
      sw.Write("fi")
416

    
417
    return
418

    
419
  def _CompleteArguments(self, sw):
420
    if not (self.opts or self.args):
421
      return
422

    
423
    all_option_names = []
424
    for opt in self.opts:
425
      all_option_names.extend(opt.all_names)
426
    all_option_names.sort()
427

    
428
    # List options if no argument has been specified yet
429
    sw.Write("_ganeti_list_options %s",
430
             utils.ShellQuote(" ".join(all_option_names)))
431

    
432
    if self.args:
433
      last_idx = len(self.args) - 1
434
      last_arg_end = 0
435
      varlen_arg_idx = None
436
      wrote_arg = False
437

    
438
      # Write some debug comments
439
      for idx, arg in enumerate(self.args):
440
        sw.Write("# %s: %r", idx, arg)
441

    
442
      sw.Write("compgenargs=")
443

    
444
      for idx, arg in enumerate(self.args):
445
        assert arg.min is not None and arg.min >= 0
446
        assert not (idx < last_idx and arg.max is None)
447

    
448
        if arg.min != arg.max or arg.max is None:
449
          if varlen_arg_idx is not None:
450
            raise Exception("Only one argument can have a variable length")
451
          varlen_arg_idx = idx
452

    
453
        compgenargs = []
454

    
455
        if isinstance(arg, cli.ArgUnknown):
456
          choices = ""
457
        elif isinstance(arg, cli.ArgSuggest):
458
          choices = utils.ShellQuote(" ".join(arg.choices))
459
        elif isinstance(arg, cli.ArgInstance):
460
          choices = "$(_ganeti_instances)"
461
        elif isinstance(arg, cli.ArgNode):
462
          choices = "$(_ganeti_nodes)"
463
        elif isinstance(arg, cli.ArgJobId):
464
          choices = "$(_ganeti_jobs)"
465
        elif isinstance(arg, cli.ArgOs):
466
          choices = "$(_ganeti_os)"
467
        elif isinstance(arg, cli.ArgFile):
468
          choices = ""
469
          compgenargs.append("-f")
470
        elif isinstance(arg, cli.ArgCommand):
471
          choices = ""
472
          compgenargs.append("-c")
473
        elif isinstance(arg, cli.ArgHost):
474
          choices = ""
475
          compgenargs.append("-A hostname")
476
        else:
477
          raise Exception("Unknown argument type %r" % arg)
478

    
479
        if arg.min == 1 and arg.max == 1:
480
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
481
        elif arg.max is None:
482
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
483
        elif arg.min <= arg.max:
484
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
485
                     (last_arg_end, last_arg_end + arg.max))
486
        else:
487
          raise Exception("Unable to generate argument position condition")
488

    
489
        last_arg_end += arg.min
490

    
491
        if choices or compgenargs:
492
          if wrote_arg:
493
            condcmd = "elif"
494
          else:
495
            condcmd = "if"
496

    
497
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
498
          sw.IncIndent()
499
          try:
500
            if choices:
501
              sw.Write("""choices="$choices "%s""", choices)
502
            if compgenargs:
503
              sw.Write("compgenargs=%s",
504
                       utils.ShellQuote(" ".join(compgenargs)))
505
          finally:
506
            sw.DecIndent()
507

    
508
          wrote_arg = True
509

    
510
      if wrote_arg:
511
        sw.Write("fi")
512

    
513
    if self.args:
514
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
515
    else:
516
      # $compgenargs exists only if there are arguments
517
      WriteCompReply(sw, '-W "$choices"')
518

    
519
  def WriteTo(self, sw):
520
    self._FindFirstArgument(sw)
521
    self._CompleteOptionValues(sw)
522
    self._CompleteArguments(sw)
523

    
524

    
525
def WriteCompletion(sw, scriptname, funcname,
526
                    commands=None,
527
                    opts=None, args=None):
528
  """Writes the completion code for one command.
529

    
530
  @type sw: ShellWriter
531
  @param sw: Script writer
532
  @type scriptname: string
533
  @param scriptname: Name of command line program
534
  @type funcname: string
535
  @param funcname: Shell function name
536
  @type commands: list
537
  @param commands: List of all subcommands in this program
538

    
539
  """
540
  sw.Write("%s() {", funcname)
541
  sw.IncIndent()
542
  try:
543
    sw.Write("local "
544
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
545
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
546
             ' i first_arg_idx choices compgenargs arg_idx optcur')
547

    
548
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
549
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
550
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
551

    
552
    sw.Write("COMPREPLY=()")
553

    
554
    if opts is not None and args is not None:
555
      assert not commands
556
      CompletionWriter(0, opts, args).WriteTo(sw)
557

    
558
    else:
559
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
560
      sw.IncIndent()
561
      try:
562
        # Complete the command name
563
        WriteCompReply(sw,
564
                       ("-W %s" %
565
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
566
      finally:
567
        sw.DecIndent()
568
      sw.Write("fi")
569

    
570
      # We're doing options and arguments to commands
571
      sw.Write("""case "${COMP_WORDS[1]}" in""")
572
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
573
        if not (argdef or optdef):
574
          continue
575

    
576
        # TODO: Group by arguments and options
577
        sw.Write("%s)", utils.ShellQuote(cmd))
578
        sw.IncIndent()
579
        try:
580
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
581
        finally:
582
          sw.DecIndent()
583

    
584
        sw.Write(";;")
585
      sw.Write("esac")
586
  finally:
587
    sw.DecIndent()
588
  sw.Write("}")
589

    
590
  sw.Write("complete -F %s -o filenames %s",
591
           utils.ShellQuote(funcname),
592
           utils.ShellQuote(scriptname))
593

    
594

    
595
def GetFunctionName(name):
596
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
597

    
598

    
599
def GetCommands(filename, module):
600
  """Returns the commands defined in a module.
601

    
602
  Aliases are also added as commands.
603

    
604
  """
605
  try:
606
    commands = getattr(module, "commands")
607
  except AttributeError:
608
    raise Exception("Script %s doesn't have 'commands' attribute" %
609
                    filename)
610

    
611
  # Add the implicit "--help" option
612
  help_option = cli.cli_option("-h", "--help", default=False,
613
                               action="store_true")
614

    
615
  for (_, _, optdef, _, _) in commands.itervalues():
616
    if help_option not in optdef:
617
      optdef.append(help_option)
618
    if cli.DEBUG_OPT not in optdef:
619
      optdef.append(cli.DEBUG_OPT)
620

    
621
  # Use aliases
622
  aliases = getattr(module, "aliases", {})
623
  if aliases:
624
    commands = commands.copy()
625
    for name, target in aliases.iteritems():
626
      commands[name] = commands[target]
627

    
628
  return commands
629

    
630

    
631
def main():
632
  buf = StringIO()
633
  sw = ShellWriter(buf)
634

    
635
  WritePreamble(sw)
636

    
637
  # gnt-* scripts
638
  for scriptname in _autoconf.GNT_SCRIPTS:
639
    filename = "scripts/%s" % scriptname
640

    
641
    WriteCompletion(sw, scriptname,
642
                    GetFunctionName(scriptname),
643
                    commands=GetCommands(filename,
644
                                         build.LoadModule(filename)))
645

    
646
  # Burnin script
647
  burnin = build.LoadModule("tools/burnin")
648
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
649
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
650

    
651
  print buf.getvalue()
652

    
653

    
654
if __name__ == "__main__":
655
  main()