Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ f59418db

History | View | Annotate | Download (17.5 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
import os
23
import re
24
from cStringIO import StringIO
25

    
26
from ganeti import constants
27
from ganeti import cli
28
from ganeti import utils
29
from ganeti import build
30

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

    
35

    
36
class ShellWriter:
37
  """Helper class to write scripts with indentation.
38

    
39
  """
40
  INDENT_STR = "  "
41

    
42
  def __init__(self, fh):
43
    self._fh = fh
44
    self._indent = 0
45

    
46
  def IncIndent(self):
47
    """Increase indentation level by 1.
48

    
49
    """
50
    self._indent += 1
51

    
52
  def DecIndent(self):
53
    """Decrease indentation level by 1.
54

    
55
    """
56
    assert self._indent > 0
57
    self._indent -= 1
58

    
59
  def Write(self, txt, *args):
60
    """Write line to output file.
61

    
62
    """
63
    self._fh.write(self._indent * self.INDENT_STR)
64

    
65
    if args:
66
      self._fh.write(txt % args)
67
    else:
68
      self._fh.write(txt)
69

    
70
    self._fh.write("\n")
71

    
72

    
73
def WritePreamble(sw):
74
  """Writes the script preamble.
75

    
76
  Helper functions should be written here.
77

    
78
  """
79
  sw.Write("# This script is automatically generated at build time.")
80
  sw.Write("# Do not modify manually.")
81

    
82
  sw.Write("_ganeti_dbglog() {")
83
  sw.IncIndent()
84
  try:
85
    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
86
    sw.IncIndent()
87
    try:
88
      sw.Write("{")
89
      sw.IncIndent()
90
      try:
91
        sw.Write("echo ---")
92
        sw.Write("echo \"$@\"")
93
        sw.Write("echo")
94
      finally:
95
        sw.DecIndent()
96
      sw.Write("} >> $GANETI_COMPL_LOG")
97
    finally:
98
      sw.DecIndent()
99
    sw.Write("fi")
100
  finally:
101
    sw.DecIndent()
102
  sw.Write("}")
103

    
104
  sw.Write("_ganeti_nodes() {")
105
  sw.IncIndent()
106
  try:
107
    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
108
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
109
  finally:
110
    sw.DecIndent()
111
  sw.Write("}")
112

    
113
  sw.Write("_ganeti_instances() {")
114
  sw.IncIndent()
115
  try:
116
    instance_list_path = os.path.join(constants.DATA_DIR,
117
                                      "ssconf_instance_list")
118
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
119
  finally:
120
    sw.DecIndent()
121
  sw.Write("}")
122

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

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

    
150
  # Params: <offset> <options with values> <options without values>
151
  # Result variable: $first_arg_idx
152
  sw.Write("_ganeti_find_first_arg() {")
153
  sw.IncIndent()
154
  try:
155
    sw.Write("local w i")
156

    
157
    sw.Write("first_arg_idx=")
158
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
159
    sw.IncIndent()
160
    try:
161
      sw.Write("w=${COMP_WORDS[$i]}")
162

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

    
166
      # Skip
167
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
168

    
169
      # Ah, we found the first argument
170
      sw.Write("else first_arg_idx=$i; break;")
171
      sw.Write("fi")
172
    finally:
173
      sw.DecIndent()
174
    sw.Write("done")
175
  finally:
176
    sw.DecIndent()
177
  sw.Write("}")
178

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

    
202
    # Calculate position of current argument
203
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
204
    sw.Write("choices=")
205
  finally:
206
    sw.DecIndent()
207
  sw.Write("}")
208

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

    
230
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
231

    
232
    sw.Write("return 1")
233
  finally:
234
    sw.DecIndent()
235
  sw.Write("}")
236

    
237
  # Params: <compgen options>
238
  # Result variable: $COMPREPLY
239
  sw.Write("_ganeti_compgen() {")
240
  sw.IncIndent()
241
  try:
242
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
243
    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
244
  finally:
245
    sw.DecIndent()
246
  sw.Write("}")
247

    
248

    
249
def WriteCompReply(sw, args, cur="\"$cur\""):
250
  sw.Write("_ganeti_compgen %s -- %s", args, cur)
251
  sw.Write("return")
252

    
253

    
254
class CompletionWriter:
255
  """Command completion writer class.
256

    
257
  """
258
  def __init__(self, arg_offset, opts, args):
259
    self.arg_offset = arg_offset
260
    self.opts = opts
261
    self.args = args
262

    
263
    for opt in opts:
264
      # While documented, these variables aren't seen as public attributes by
265
      # pylint. pylint: disable-msg=W0212
266
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
267

    
268
  def _FindFirstArgument(self, sw):
269
    ignore = []
270
    skip_one = []
271

    
272
    for opt in self.opts:
273
      if opt.takes_value():
274
        # Ignore value
275
        for i in opt.all_names:
276
          if i.startswith("--"):
277
            ignore.append("%s=*" % utils.ShellQuote(i))
278
          skip_one.append(utils.ShellQuote(i))
279
      else:
280
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
281

    
282
    ignore = sorted(utils.UniqueSequence(ignore))
283
    skip_one = sorted(utils.UniqueSequence(skip_one))
284

    
285
    if ignore or skip_one:
286
      # Try to locate first argument
287
      sw.Write("_ganeti_find_first_arg %s %s %s",
288
               self.arg_offset + 1,
289
               utils.ShellQuote("|".join(skip_one)),
290
               utils.ShellQuote("|".join(ignore)))
291
    else:
292
      # When there are no options the first argument is always at position
293
      # offset + 1
294
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)
295

    
296
  def _CompleteOptionValues(self, sw):
297
    # Group by values
298
    # "values" -> [optname1, optname2, ...]
299
    values = {}
300

    
301
    for opt in self.opts:
302
      if not opt.takes_value():
303
        continue
304

    
305
      # Only static choices implemented so far (e.g. no node list)
306
      suggest = getattr(opt, "completion_suggest", None)
307

    
308
      if not suggest:
309
        suggest = opt.choices
310

    
311
      if (isinstance(suggest, (int, long)) and
312
          suggest in cli.OPT_COMPL_ALL):
313
        key = suggest
314
      elif suggest:
315
        key = " ".join(sorted(suggest))
316
      else:
317
        key = ""
318

    
319
      values.setdefault(key, []).extend(opt.all_names)
320

    
321
    # Don't write any code if there are no option values
322
    if not values:
323
      return
324

    
325
    cur = "\"$optcur\""
326

    
327
    wrote_opt = False
328

    
329
    for (suggest, allnames) in values.iteritems():
330
      longnames = [i for i in allnames if i.startswith("--")]
331

    
332
      if wrote_opt:
333
        condcmd = "elif"
334
      else:
335
        condcmd = "if"
336

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

    
356
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
357
          sw.IncIndent()
358
          try:
359
            sw.Write("node1=\"${optcur%%:*}\"")
360

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

    
372
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
373
                   " node1=\"'$node1'\"")
374

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

    
395
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
396
        else:
397
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
398
      finally:
399
        sw.DecIndent()
400

    
401
      wrote_opt = True
402

    
403
    if wrote_opt:
404
      sw.Write("fi")
405

    
406
    return
407

    
408
  def _CompleteArguments(self, sw):
409
    if not (self.opts or self.args):
410
      return
411

    
412
    all_option_names = []
413
    for opt in self.opts:
414
      all_option_names.extend(opt.all_names)
415
    all_option_names.sort()
416

    
417
    # List options if no argument has been specified yet
418
    sw.Write("_ganeti_list_options %s",
419
             utils.ShellQuote(" ".join(all_option_names)))
420

    
421
    if self.args:
422
      last_idx = len(self.args) - 1
423
      last_arg_end = 0
424
      varlen_arg_idx = None
425
      wrote_arg = False
426

    
427
      # Write some debug comments
428
      for idx, arg in enumerate(self.args):
429
        sw.Write("# %s: %r", idx, arg)
430

    
431
      sw.Write("compgenargs=")
432

    
433
      for idx, arg in enumerate(self.args):
434
        assert arg.min is not None and arg.min >= 0
435
        assert not (idx < last_idx and arg.max is None)
436

    
437
        if arg.min != arg.max or arg.max is None:
438
          if varlen_arg_idx is not None:
439
            raise Exception("Only one argument can have a variable length")
440
          varlen_arg_idx = idx
441

    
442
        compgenargs = []
443

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

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

    
476
        last_arg_end += arg.min
477

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

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

    
495
          wrote_arg = True
496

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

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

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

    
511

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

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

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

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

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

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

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

    
557
      # We're doing options and arguments to commands
558
      sw.Write("""case "${COMP_WORDS[1]}" in""")
559
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
560
        if not (argdef or optdef):
561
          continue
562

    
563
        # TODO: Group by arguments and options
564
        sw.Write("%s)", utils.ShellQuote(cmd))
565
        sw.IncIndent()
566
        try:
567
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
568
        finally:
569
          sw.DecIndent()
570

    
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 (_, _, optdef, _, _) in commands.itervalues():
603
    if help_option not in optdef:
604
      optdef.append(help_option)
605
    if cli.DEBUG_OPT not in optdef:
606
      optdef.append(cli.DEBUG_OPT)
607

    
608
  # Use aliases
609
  aliases = getattr(module, "aliases", {})
610
  if aliases:
611
    commands = commands.copy()
612
    for name, target in aliases.iteritems():
613
      commands[name] = commands[target]
614

    
615
  return commands
616

    
617

    
618
def main():
619
  buf = StringIO()
620
  sw = ShellWriter(buf)
621

    
622
  WritePreamble(sw)
623

    
624
  # gnt-* scripts
625
  for scriptname in _autoconf.GNT_SCRIPTS:
626
    filename = "scripts/%s" % scriptname
627

    
628
    WriteCompletion(sw, scriptname,
629
                    GetFunctionName(scriptname),
630
                    commands=GetCommands(filename,
631
                                         build.LoadModule(filename)))
632

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

    
638
  print buf.getvalue()
639

    
640

    
641
if __name__ == "__main__":
642
  main()