Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 064c21f8

History | View | Annotate | Download (17.1 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

    
240
def WriteCompReply(sw, args, cur="\"$cur\""):
241
  sw.Write("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur)
242
  sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
243
  sw.Write("return")
244

    
245

    
246
class CompletionWriter:
247
  """Command completion writer class.
248

    
249
  """
250
  def __init__(self, arg_offset, opts, args):
251
    self.arg_offset = arg_offset
252
    self.opts = opts
253
    self.args = args
254

    
255
    for opt in opts:
256
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
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
      if not suggest:
299
        suggest = opt.choices
300

    
301
      if (isinstance(suggest, (int, long)) and
302
          suggest in cli.OPT_COMPL_ALL):
303
        key = suggest
304
      elif suggest:
305
        key = " ".join(sorted(suggest))
306
      else:
307
        key = ""
308

    
309
      values.setdefault(key, []).extend(opt.all_names)
310

    
311
    # Don't write any code if there are no option values
312
    if not values:
313
      return
314

    
315
    cur = "\"$optcur\""
316

    
317
    wrote_opt = False
318

    
319
    for (suggest, allnames) in values.iteritems():
320
      longnames = [i for i in allnames if i.startswith("--")]
321

    
322
      if wrote_opt:
323
        condcmd = "elif"
324
      else:
325
        condcmd = "if"
326

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

    
346
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
347
          sw.IncIndent()
348
          try:
349
            sw.Write("node1=\"${optcur%%:*}\"")
350

    
351
            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
352
            sw.IncIndent()
353
            try:
354
              sw.Write("pfx=\"$node1:\"")
355
            finally:
356
              sw.DecIndent()
357
            sw.Write("fi")
358
          finally:
359
            sw.DecIndent()
360
          sw.Write("fi")
361

    
362
          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
363
                   " node1=\"'$node1'\"")
364

    
365
          sw.Write("for i in $(_ganeti_nodes); do")
366
          sw.IncIndent()
367
          try:
368
            sw.Write("if [[ -z \"$node1\" ]]; then")
369
            sw.IncIndent()
370
            try:
371
              sw.Write("tmp=\"$tmp $i $i:\"")
372
            finally:
373
              sw.DecIndent()
374
            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
375
            sw.IncIndent()
376
            try:
377
              sw.Write("tmp=\"$tmp $i\"")
378
            finally:
379
              sw.DecIndent()
380
            sw.Write("fi")
381
          finally:
382
            sw.DecIndent()
383
          sw.Write("done")
384

    
385
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
386
        else:
387
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
388
      finally:
389
        sw.DecIndent()
390

    
391
      wrote_opt = True
392

    
393
    if wrote_opt:
394
      sw.Write("fi")
395

    
396
    return
397

    
398
  def _CompleteArguments(self, sw):
399
    if not (self.opts or self.args):
400
      return
401

    
402
    all_option_names = []
403
    for opt in self.opts:
404
      all_option_names.extend(opt.all_names)
405
    all_option_names.sort()
406

    
407
    # List options if no argument has been specified yet
408
    sw.Write("_ganeti_list_options %s",
409
             utils.ShellQuote(" ".join(all_option_names)))
410

    
411
    if self.args:
412
      last_idx = len(self.args) - 1
413
      last_arg_end = 0
414
      varlen_arg_idx = None
415
      wrote_arg = False
416

    
417
      # Write some debug comments
418
      for idx, arg in enumerate(self.args):
419
        sw.Write("# %s: %r", idx, arg)
420

    
421
      sw.Write("compgenargs=")
422

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

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

    
432
        compgenargs = []
433

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

    
456
        if arg.min == 1 and arg.max == 1:
457
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
458
        elif arg.min <= arg.max:
459
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
460
                     (last_arg_end, last_arg_end + arg.max))
461
        elif arg.max is None:
462
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
463
        else:
464
          raise Exception("Unable to generate argument position condition")
465

    
466
        last_arg_end += arg.min
467

    
468
        if choices or compgenargs:
469
          if wrote_arg:
470
            condcmd = "elif"
471
          else:
472
            condcmd = "if"
473

    
474
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
475
          sw.IncIndent()
476
          try:
477
            if choices:
478
              sw.Write("""choices="$choices "%s""", choices)
479
            if compgenargs:
480
              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
481
          finally:
482
            sw.DecIndent()
483

    
484
          wrote_arg = True
485

    
486
      if wrote_arg:
487
        sw.Write("fi")
488

    
489
    if self.args:
490
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
491
    else:
492
      # $compgenargs exists only if there are arguments
493
      WriteCompReply(sw, '-W "$choices"')
494

    
495
  def WriteTo(self, sw):
496
    self._FindFirstArgument(sw)
497
    self._CompleteOptionValues(sw)
498
    self._CompleteArguments(sw)
499

    
500

    
501
def WriteCompletion(sw, scriptname, funcname,
502
                    commands=None,
503
                    opts=None, args=None):
504
  """Writes the completion code for one command.
505

    
506
  @type sw: ShellWriter
507
  @param sw: Script writer
508
  @type scriptname: string
509
  @param scriptname: Name of command line program
510
  @type funcname: string
511
  @param funcname: Shell function name
512
  @type commands: list
513
  @param commands: List of all subcommands in this program
514

    
515
  """
516
  sw.Write("%s() {", funcname)
517
  sw.IncIndent()
518
  try:
519
    sw.Write("local "
520
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
521
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
522
             ' i first_arg_idx choices compgenargs arg_idx optcur')
523

    
524
    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
525
    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
526
             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
527

    
528
    sw.Write("COMPREPLY=()")
529

    
530
    if opts is not None and args is not None:
531
      assert not commands
532
      CompletionWriter(0, opts, args).WriteTo(sw)
533

    
534
    else:
535
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
536
      sw.IncIndent()
537
      try:
538
        # Complete the command name
539
        WriteCompReply(sw,
540
                       ("-W %s" %
541
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
542
      finally:
543
        sw.DecIndent()
544
      sw.Write("fi")
545

    
546
      # We're doing options and arguments to commands
547
      sw.Write("""case "${COMP_WORDS[1]}" in""")
548
      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
549
        if not (argdef or optdef):
550
          continue
551

    
552
        # TODO: Group by arguments and options
553
        sw.Write("%s)", utils.ShellQuote(cmd))
554
        sw.IncIndent()
555
        try:
556
          CompletionWriter(1, optdef, argdef).WriteTo(sw)
557
        finally:
558
          sw.DecIndent()
559

    
560
        sw.Write(";;")
561
      sw.Write("esac")
562
  finally:
563
    sw.DecIndent()
564
  sw.Write("}")
565

    
566
  sw.Write("complete -F %s -o filenames %s",
567
           utils.ShellQuote(funcname),
568
           utils.ShellQuote(scriptname))
569

    
570

    
571
def GetFunctionName(name):
572
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
573

    
574

    
575
def GetCommands(filename, module):
576
  """Returns the commands defined in a module.
577

    
578
  Aliases are also added as commands.
579

    
580
  """
581
  try:
582
    commands = getattr(module, "commands")
583
  except AttributeError, err:
584
    raise Exception("Script %s doesn't have 'commands' attribute" %
585
                    filename)
586

    
587
  # Add the implicit "--help" option
588
  help_option = cli.cli_option("-h", "--help", default=False,
589
                               action="store_true")
590

    
591
  for (_, _, optdef, _, _) in commands.itervalues():
592
    if help_option not in optdef:
593
      optdef.append(help_option)
594
    if cli.DEBUG_OPT not in optdef:
595
      optdef.append(cli.DEBUG_OPT)
596

    
597
  # Use aliases
598
  aliases = getattr(module, "aliases", {})
599
  if aliases:
600
    commands = commands.copy()
601
    for name, target in aliases.iteritems():
602
      commands[name] = commands[target]
603

    
604
  return commands
605

    
606

    
607
def main():
608
  buf = StringIO()
609
  sw = ShellWriter(buf)
610

    
611
  WritePreamble(sw)
612

    
613
  # gnt-* scripts
614
  for scriptname in _autoconf.GNT_SCRIPTS:
615
    filename = "scripts/%s" % scriptname
616

    
617
    WriteCompletion(sw, scriptname,
618
                    GetFunctionName(scriptname),
619
                    commands=GetCommands(filename,
620
                                         build.LoadModule(filename)))
621

    
622
  # Burnin script
623
  burnin = build.LoadModule("tools/burnin")
624
  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
625
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
626

    
627
  print buf.getvalue()
628

    
629

    
630
if __name__ == "__main__":
631
  main()