Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 8adfb141

History | View | Annotate | Download (17.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
import optparse
23
import os
24
import sys
25
import re
26
from cStringIO import StringIO
27

    
28
from ganeti import constants
29
from ganeti import cli
30
from ganeti import utils
31
from ganeti import build
32

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

    
37

    
38
class ShellWriter:
39
  """Helper class to write scripts with indentation.
40

    
41
  """
42
  INDENT_STR = "  "
43

    
44
  def __init__(self, fh):
45
    self._fh = fh
46
    self._indent = 0
47

    
48
  def IncIndent(self):
49
    """Increase indentation level by 1.
50

    
51
    """
52
    self._indent += 1
53

    
54
  def DecIndent(self):
55
    """Decrease indentation level by 1.
56

    
57
    """
58
    assert self._indent > 0
59
    self._indent -= 1
60

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

    
64
    """
65
    self._fh.write(self._indent * self.INDENT_STR)
66

    
67
    if args:
68
      self._fh.write(txt % args)
69
    else:
70
      self._fh.write(txt)
71

    
72
    self._fh.write("\n")
73

    
74

    
75
def WritePreamble(sw):
76
  """Writes the script preamble.
77

    
78
  Helper functions should be written here.
79

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
232
    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
233

    
234
    sw.Write("return 1")
235
  finally:
236
    sw.DecIndent()
237
  sw.Write("}")
238

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

    
250

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

    
255

    
256
class CompletionWriter:
257
  """Command completion writer class.
258

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

    
265
    for opt in opts:
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, err:
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()