Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ e37f47d3

History | View | Annotate | Download (24.1 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 os.path
31
import re
32
import itertools
33
import optparse
34
from cStringIO import StringIO
35

    
36
from ganeti import constants
37
from ganeti import cli
38
from ganeti import utils
39
from ganeti import build
40
from ganeti import pathutils
41

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

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

    
50

    
51
def WritePreamble(sw, support_debug):
52
  """Writes the script preamble.
53

    
54
  Helper functions should be written here.
55

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

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

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

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

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

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

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

    
138
  sw.Write("_ganeti_network() {")
139
  sw.IncIndent()
140
  try:
141
    networks_path = os.path.join(pathutils.DATA_DIR, "ssconf_networks")
142
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
143
  finally:
144
    sw.DecIndent()
145
  sw.Write("}")
146

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

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

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

    
163
      # Skip
164
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
165

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

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

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

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

    
227
    if support_debug:
228
      sw.Write("_gnt_log optcur=\"'$optcur'\"")
229

    
230
    sw.Write("return 1")
231
  finally:
232
    sw.DecIndent()
233
  sw.Write("}")
234

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

    
247

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

    
252

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

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

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

    
268
      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
269
      if invalid:
270
        raise Exception("Option names don't match regular expression '%s': %s" %
271
                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
272

    
273
  def _FindFirstArgument(self, sw):
274
    ignore = []
275
    skip_one = []
276

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

    
287
    ignore = sorted(utils.UniqueSequence(ignore))
288
    skip_one = sorted(utils.UniqueSequence(skip_one))
289

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

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

    
306
    for opt in self.opts:
307
      if not opt.takes_value():
308
        continue
309

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

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

    
317
      if not suggest:
318
        suggest = opt.choices
319

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

    
328
      values.setdefault(key, []).extend(opt.all_names)
329

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

    
334
    cur = "\"$optcur\""
335

    
336
    wrote_opt = False
337

    
338
    for (suggest, allnames) in values.items():
339
      longnames = [i for i in allnames if i.startswith("--")]
340

    
341
      if wrote_opt:
342
        condcmd = "elif"
343
      else:
344
        condcmd = "if"
345

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

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

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

    
385
          if self.support_debug:
386
            sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
387
                     " node1=\"'$node1'\"")
388

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

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

    
415
      wrote_opt = True
416

    
417
    if wrote_opt:
418
      sw.Write("fi")
419

    
420
    return
421

    
422
  def _CompleteArguments(self, sw):
423
    if not (self.opts or self.args):
424
      return
425

    
426
    all_option_names = []
427
    for opt in self.opts:
428
      all_option_names.extend(opt.all_names)
429
    all_option_names.sort()
430

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

    
435
    if self.args:
436
      last_idx = len(self.args) - 1
437
      last_arg_end = 0
438
      varlen_arg_idx = None
439
      wrote_arg = False
440

    
441
      sw.Write("compgenargs=")
442

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

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

    
452
        compgenargs = []
453

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

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

    
492
        last_arg_end += arg.min
493

    
494
        if choices or compgenargs:
495
          if wrote_arg:
496
            condcmd = "elif"
497
          else:
498
            condcmd = "if"
499

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

    
511
          wrote_arg = True
512

    
513
      if wrote_arg:
514
        sw.Write("fi")
515

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

    
522
  def WriteTo(self, sw):
523
    self._FindFirstArgument(sw)
524
    self._CompleteOptionValues(sw)
525
    self._CompleteArguments(sw)
526

    
527

    
528
def WriteCompletion(sw, scriptname, funcname, support_debug,
529
                    commands=None,
530
                    opts=None, args=None):
531
  """Writes the completion code for one command.
532

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

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

    
551
    if support_debug:
552
      sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
553
      sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
554
               " _gnt_log \"$(set | grep ^COMP_)\"")
555

    
556
    sw.Write("COMPREPLY=()")
557

    
558
    if opts is not None and args is not None:
559
      assert not commands
560
      CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
561

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

    
574
      # Group commands by arguments and options
575
      grouped_cmds = {}
576
      for cmd, (_, argdef, optdef, _, _) in commands.items():
577
        if not (argdef or optdef):
578
          continue
579
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
580

    
581
      # We're doing options and arguments to commands
582
      sw.Write("""case "${COMP_WORDS[1]}" in""")
583
      sort_grouped = sorted(grouped_cmds.items(),
584
                            key=lambda (_, y): sorted(y)[0])
585
      for ((argdef, optdef), cmds) in sort_grouped:
586
        assert argdef or optdef
587
        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
588
        sw.IncIndent()
589
        try:
590
          CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
591
        finally:
592
          sw.DecIndent()
593
        sw.Write(";;")
594
      sw.Write("esac")
595
  finally:
596
    sw.DecIndent()
597
  sw.Write("}")
598

    
599
  sw.Write("complete -F %s -o filenames %s",
600
           utils.ShellQuote(funcname),
601
           utils.ShellQuote(scriptname))
602

    
603

    
604
def GetFunctionName(name):
605
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
606

    
607

    
608
def GetCommands(filename, module):
609
  """Returns the commands defined in a module.
610

    
611
  Aliases are also added as commands.
612

    
613
  """
614
  try:
615
    commands = getattr(module, "commands")
616
  except AttributeError:
617
    raise Exception("Script %s doesn't have 'commands' attribute" %
618
                    filename)
619

    
620
  # Add the implicit "--help" option
621
  help_option = cli.cli_option("-h", "--help", default=False,
622
                               action="store_true")
623

    
624
  for name, (_, _, optdef, _, _) in commands.items():
625
    if help_option not in optdef:
626
      optdef.append(help_option)
627
    for opt in cli.COMMON_OPTS:
628
      if opt in optdef:
629
        raise Exception("Common option '%s' listed for command '%s' in %s" %
630
                        (opt, name, filename))
631
      optdef.append(opt)
632

    
633
  # Use aliases
634
  aliases = getattr(module, "aliases", {})
635
  if aliases:
636
    commands = commands.copy()
637
    for name, target in aliases.items():
638
      commands[name] = commands[target]
639

    
640
  return commands
641

    
642

    
643
def HaskellOptToOptParse(opts, kind):
644
  """Converts a Haskell options to Python cli_options.
645

    
646
  @type opts: string
647
  @param opts: comma-separated string with short and long options
648
  @type kind: string
649
  @param kind: type generated by Common.hs/complToText; needs to be
650
      kept in sync
651

    
652
  """
653
  # pylint: disable=W0142
654
  # since we pass *opts in a number of places
655
  opts = opts.split(",")
656
  if kind == "none":
657
    return cli.cli_option(*opts, action="store_true")
658
  elif kind in ["file", "string", "host", "dir", "inetaddr"]:
659
    return cli.cli_option(*opts, type="string")
660
  elif kind == "integer":
661
    return cli.cli_option(*opts, type="int")
662
  elif kind == "float":
663
    return cli.cli_option(*opts, type="float")
664
  elif kind == "onegroup":
665
    return cli.cli_option(*opts, type="string",
666
                           completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
667
  elif kind == "onenode":
668
    return cli.cli_option(*opts, type="string",
669
                          completion_suggest=cli.OPT_COMPL_ONE_NODE)
670
  elif kind == "manyinstances":
671
    # FIXME: no support for many instances
672
    return cli.cli_option(*opts, type="string")
673
  elif kind.startswith("choices="):
674
    choices = kind[len("choices="):].split(",")
675
    return cli.cli_option(*opts, type="choice", choices=choices)
676
  else:
677
    # FIXME: there are many other currently unused completion types,
678
    # should be added on an as-needed basis
679
    raise Exception("Unhandled option kind '%s'" % kind)
680

    
681

    
682
#: serialised kind to arg type
683
_ARG_MAP = {
684
  "choices": cli.ArgChoice,
685
  "command": cli.ArgCommand,
686
  "file": cli.ArgFile,
687
  "host": cli.ArgHost,
688
  "jobid": cli.ArgJobId,
689
  "onegroup": cli.ArgGroup,
690
  "oneinstance": cli.ArgInstance,
691
  "onenode": cli.ArgNode,
692
  "oneos": cli.ArgOs,
693
  "string": cli.ArgUnknown,
694
  "suggests": cli.ArgSuggest,
695
  }
696

    
697

    
698
def HaskellArgToCliArg(kind, min_cnt, max_cnt):
699
  """Converts a Haskell options to Python _Argument.
700

    
701
  @type kind: string
702
  @param kind: type generated by Common.hs/argComplToText; needs to be
703
      kept in sync
704

    
705
  """
706
  min_cnt = int(min_cnt)
707
  if max_cnt == "none":
708
    max_cnt = None
709
  else:
710
    max_cnt = int(max_cnt)
711
  # pylint: disable=W0142
712
  # since we pass **kwargs
713
  kwargs = {"min": min_cnt, "max": max_cnt}
714

    
715
  if kind.startswith("choices=") or kind.startswith("suggest="):
716
    (kind, choices) = kind.split("=", 1)
717
    kwargs["choices"] = choices.split(",")
718

    
719
  if kind not in _ARG_MAP:
720
    raise Exception("Unhandled argument kind '%s'" % kind)
721
  else:
722
    return _ARG_MAP[kind](**kwargs)
723

    
724

    
725
def WriteHaskellCompletion(sw, script, htools=True, debug=True):
726
  """Generates completion information for a Haskell program.
727

    
728
  This Converts completion info from a Haskell program into 'fake'
729
  cli_opts and then builds completion for them.
730

    
731
  """
732
  if htools:
733
    cmd = "./htools/htools"
734
    env = {"HTOOLS": script}
735
    script_name = script
736
    func_name = "htools_%s" % script
737
  else:
738
    cmd = "./" + script
739
    env = {}
740
    script_name = os.path.basename(script)
741
    func_name = script_name
742
  func_name = func_name.replace("-", "_")
743
  output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
744
  cli_opts = []
745
  args = []
746
  for line in output.splitlines():
747
    v = line.split(None)
748
    exc = lambda msg: Exception("Invalid %s output from %s: %s" %
749
                                (msg, script, v))
750
    if len(v) < 2:
751
      raise exc("help completion")
752
    if v[0].startswith("-"):
753
      if len(v) != 2:
754
        raise exc("option format")
755
      (opts, kind) = v
756
      cli_opts.append(HaskellOptToOptParse(opts, kind))
757
    else:
758
      if len(v) != 3:
759
        raise exc("argument format")
760
      (kind, min_cnt, max_cnt) = v
761
      args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
762
  WriteCompletion(sw, script_name, func_name, debug, opts=cli_opts, args=args)
763

    
764

    
765
def main():
766
  parser = optparse.OptionParser(usage="%prog [--compact]")
767
  parser.add_option("--compact", action="store_true",
768
                    help=("Don't indent output and don't include debugging"
769
                          " facilities"))
770

    
771
  options, args = parser.parse_args()
772
  if args:
773
    parser.error("Wrong number of arguments")
774

    
775
  buf = StringIO()
776
  sw = utils.ShellWriter(buf, indent=not options.compact)
777

    
778
  # Remember original state of extglob and enable it (required for pattern
779
  # matching; must be enabled while parsing script)
780
  sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
781
  sw.Write("shopt -s extglob")
782

    
783
  WritePreamble(sw, not options.compact)
784

    
785
  # gnt-* scripts
786
  for scriptname in _autoconf.GNT_SCRIPTS:
787
    filename = "scripts/%s" % scriptname
788

    
789
    WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
790
                    not options.compact,
791
                    commands=GetCommands(filename,
792
                                         build.LoadModule(filename)))
793

    
794
  # Burnin script
795
  burnin = build.LoadModule("tools/burnin")
796
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
797
                  not options.compact,
798
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
799

    
800
  # ganeti-cleaner
801
  WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
802
                         debug=not options.compact)
803

    
804
  # htools, if enabled
805
  if _autoconf.HTOOLS:
806
    for script in _autoconf.HTOOLS_PROGS:
807
      WriteHaskellCompletion(sw, script, htools=True,
808
                             debug=not options.compact)
809

    
810
  # ganeti-confd, if enabled
811
  if _autoconf.ENABLE_CONFD:
812
    WriteHaskellCompletion(sw, "htools/ganeti-confd", htools=False,
813
                           debug=not options.compact)
814

    
815
  # Reset extglob to original value
816
  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
817
  sw.Write("unset gnt_shopt_extglob")
818

    
819
  print buf.getvalue()
820

    
821

    
822
if __name__ == "__main__":
823
  main()