Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ fad06963

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

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

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

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

    
49

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

    
53
  Helper functions should be written here.
54

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

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

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

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

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

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

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

    
137
  # Params: <offset> <options with values> <options without values>
138
  # Result variable: $first_arg_idx
139
  sw.Write("_ganeti_find_first_arg() {")
140
  sw.IncIndent()
141
  try:
142
    sw.Write("local w i")
143

    
144
    sw.Write("first_arg_idx=")
145
    sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
146
    sw.IncIndent()
147
    try:
148
      sw.Write("w=${COMP_WORDS[$i]}")
149

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

    
153
      # Skip
154
      sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
155

    
156
      # Ah, we found the first argument
157
      sw.Write("else first_arg_idx=$i; break;")
158
      sw.Write("fi")
159
    finally:
160
      sw.DecIndent()
161
    sw.Write("done")
162
  finally:
163
    sw.DecIndent()
164
  sw.Write("}")
165

    
166
  # Params: <list of options separated by space>
167
  # Input variable: $first_arg_idx
168
  # Result variables: $arg_idx, $choices
169
  sw.Write("_ganeti_list_options() {")
170
  sw.IncIndent()
171
  try:
172
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
173
    sw.IncIndent()
174
    try:
175
      sw.Write("arg_idx=0")
176
      # Show options only if the current word starts with a dash
177
      sw.Write("""if [[ "$cur" == -* ]]; then""")
178
      sw.IncIndent()
179
      try:
180
        sw.Write("choices=$1")
181
      finally:
182
        sw.DecIndent()
183
      sw.Write("fi")
184
      sw.Write("return")
185
    finally:
186
      sw.DecIndent()
187
    sw.Write("fi")
188

    
189
    # Calculate position of current argument
190
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
191
    sw.Write("choices=")
192
  finally:
193
    sw.DecIndent()
194
  sw.Write("}")
195

    
196
  # Params: <long options with equal sign> <all options>
197
  # Result variable: $optcur
198
  sw.Write("_gnt_checkopt() {")
199
  sw.IncIndent()
200
  try:
201
    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
202
    sw.IncIndent()
203
    try:
204
      sw.Write("optcur=\"${cur#--*=}\"")
205
      sw.Write("return 0")
206
    finally:
207
      sw.DecIndent()
208
    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
209
    sw.IncIndent()
210
    try:
211
      sw.Write("optcur=\"$cur\"")
212
      sw.Write("return 0")
213
    finally:
214
      sw.DecIndent()
215
    sw.Write("fi")
216

    
217
    if support_debug:
218
      sw.Write("_gnt_log optcur=\"'$optcur'\"")
219

    
220
    sw.Write("return 1")
221
  finally:
222
    sw.DecIndent()
223
  sw.Write("}")
224

    
225
  # Params: <compgen options>
226
  # Result variable: $COMPREPLY
227
  sw.Write("_gnt_compgen() {")
228
  sw.IncIndent()
229
  try:
230
    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
231
    if support_debug:
232
      sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
233
  finally:
234
    sw.DecIndent()
235
  sw.Write("}")
236

    
237

    
238
def WriteCompReply(sw, args, cur="\"$cur\""):
239
  sw.Write("_gnt_compgen %s -- %s", args, cur)
240
  sw.Write("return")
241

    
242

    
243
class CompletionWriter:
244
  """Command completion writer class.
245

    
246
  """
247
  def __init__(self, arg_offset, opts, args, support_debug):
248
    self.arg_offset = arg_offset
249
    self.opts = opts
250
    self.args = args
251
    self.support_debug = support_debug
252

    
253
    for opt in opts:
254
      # While documented, these variables aren't seen as public attributes by
255
      # pylint. pylint: disable=W0212
256
      opt.all_names = sorted(opt._short_opts + opt._long_opts)
257

    
258
      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
259
      if invalid:
260
        raise Exception("Option names don't match regular expression '%s': %s" %
261
                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
262

    
263
  def _FindFirstArgument(self, sw):
264
    ignore = []
265
    skip_one = []
266

    
267
    for opt in self.opts:
268
      if opt.takes_value():
269
        # Ignore value
270
        for i in opt.all_names:
271
          if i.startswith("--"):
272
            ignore.append("%s=*" % utils.ShellQuote(i))
273
          skip_one.append(utils.ShellQuote(i))
274
      else:
275
        ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
276

    
277
    ignore = sorted(utils.UniqueSequence(ignore))
278
    skip_one = sorted(utils.UniqueSequence(skip_one))
279

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

    
291
  def _CompleteOptionValues(self, sw):
292
    # Group by values
293
    # "values" -> [optname1, optname2, ...]
294
    values = {}
295

    
296
    for opt in self.opts:
297
      if not opt.takes_value():
298
        continue
299

    
300
      # Only static choices implemented so far (e.g. no node list)
301
      suggest = getattr(opt, "completion_suggest", None)
302

    
303
      # our custom option type
304
      if opt.type == "bool":
305
        suggest = ["yes", "no"]
306

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

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

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

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

    
324
    cur = "\"$optcur\""
325

    
326
    wrote_opt = False
327

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

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

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

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

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

    
373
          if self.support_debug:
374
            sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
375
                     " node1=\"'$node1'\"")
376

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

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

    
403
      wrote_opt = True
404

    
405
    if wrote_opt:
406
      sw.Write("fi")
407

    
408
    return
409

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

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

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

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

    
429
      sw.Write("compgenargs=")
430

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

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

    
440
        compgenargs = []
441

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

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

    
478
        last_arg_end += arg.min
479

    
480
        if choices or compgenargs:
481
          if wrote_arg:
482
            condcmd = "elif"
483
          else:
484
            condcmd = "if"
485

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

    
497
          wrote_arg = True
498

    
499
      if wrote_arg:
500
        sw.Write("fi")
501

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

    
508
  def WriteTo(self, sw):
509
    self._FindFirstArgument(sw)
510
    self._CompleteOptionValues(sw)
511
    self._CompleteArguments(sw)
512

    
513

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

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

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

    
537
    if support_debug:
538
      sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
539
      sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
540
               " _gnt_log \"$(set | grep ^COMP_)\"")
541

    
542
    sw.Write("COMPREPLY=()")
543

    
544
    if opts is not None and args is not None:
545
      assert not commands
546
      CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
547

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

    
560
      # Group commands by arguments and options
561
      grouped_cmds = {}
562
      for cmd, (_, argdef, optdef, _, _) in commands.items():
563
        if not (argdef or optdef):
564
          continue
565
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
566

    
567
      # We're doing options and arguments to commands
568
      sw.Write("""case "${COMP_WORDS[1]}" in""")
569
      sort_grouped = sorted(grouped_cmds.items(),
570
                            key=lambda (_, y): sorted(y)[0])
571
      for ((argdef, optdef), cmds) in sort_grouped:
572
        assert argdef or optdef
573
        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
574
        sw.IncIndent()
575
        try:
576
          CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
577
        finally:
578
          sw.DecIndent()
579
        sw.Write(";;")
580
      sw.Write("esac")
581
  finally:
582
    sw.DecIndent()
583
  sw.Write("}")
584

    
585
  sw.Write("complete -F %s -o filenames %s",
586
           utils.ShellQuote(funcname),
587
           utils.ShellQuote(scriptname))
588

    
589

    
590
def GetFunctionName(name):
591
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
592

    
593

    
594
def GetCommands(filename, module):
595
  """Returns the commands defined in a module.
596

    
597
  Aliases are also added as commands.
598

    
599
  """
600
  try:
601
    commands = getattr(module, "commands")
602
  except AttributeError:
603
    raise Exception("Script %s doesn't have 'commands' attribute" %
604
                    filename)
605

    
606
  # Add the implicit "--help" option
607
  help_option = cli.cli_option("-h", "--help", default=False,
608
                               action="store_true")
609

    
610
  for name, (_, _, optdef, _, _) in commands.items():
611
    if help_option not in optdef:
612
      optdef.append(help_option)
613
    for opt in cli.COMMON_OPTS:
614
      if opt in optdef:
615
        raise Exception("Common option '%s' listed for command '%s' in %s" %
616
                        (opt, name, filename))
617
      optdef.append(opt)
618

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

    
626
  return commands
627

    
628

    
629
def HaskellOptToOptParse(opts, kind):
630
  """Converts a Haskell options to Python cli_options.
631

    
632
  @type opts: string
633
  @param opts: comma-separated string with short and long options
634
  @type kind: string
635
  @param kind: type generated by Common.hs/complToText; needs to be
636
      kept in sync
637

    
638
  """
639
  # pylint: disable=W0142
640
  # since we pass *opts in a number of places
641
  opts = opts.split(",")
642
  if kind == "none":
643
    return cli.cli_option(*opts, action="store_true")
644
  elif kind in ["file", "string", "host", "dir"]:
645
    return cli.cli_option(*opts, type="string")
646
  elif kind == "numeric":
647
    # FIXME: float values here
648
    return cli.cli_option(*opts, type="string")
649
  elif kind == "onegroup":
650
    return cli.cli_option(*opts, type="string",
651
                           completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
652
  elif kind == "onenode":
653
    return cli.cli_option(*opts, type="string",
654
                          completion_suggest=cli.OPT_COMPL_ONE_NODE)
655
  elif kind == "manyinstances":
656
    # FIXME: no support for many instances
657
    return cli.cli_option(*opts, type="string")
658
  elif kind.startswith("choices="):
659
    choices = kind[len("choices="):].split(",")
660
    return cli.cli_option(*opts, type="choice", choices=choices)
661
  else:
662
    # FIXME: there are many other currently unused completion types,
663
    # should be added on an as-needed basis
664
    raise Exception("Unhandled option kind '%s'" % kind)
665

    
666

    
667
#: serialised kind to arg type
668
_ARG_MAP = {
669
  "command": cli.ArgCommand,
670
  "file": cli.ArgFile,
671
  "host": cli.ArgHost,
672
  "jobid": cli.ArgJobId,
673
  "onegroup": cli.ArgGroup,
674
  "oneinstance": cli.ArgInstance,
675
  "onenode": cli.ArgNode,
676
  "oneos": cli.ArgOs,
677
  "string": cli.ArgUnknown,
678
  }
679

    
680

    
681
def HaskellArgToCliArg(kind, min_cnt, max_cnt):
682
  """Converts a Haskell options to Python _Argument.
683

    
684
  @type kind: string
685
  @param kind: type generated by Common.hs/argComplToText; needs to be
686
      kept in sync
687

    
688
  """
689
  min_cnt = int(min_cnt)
690
  if max_cnt == "none":
691
    max_cnt = None
692
  else:
693
    max_cnt = int(max_cnt)
694
  # pylint: disable=W0142
695
  # since we pass **kwargs
696
  kwargs = {"min": min_cnt, "max": max_cnt}
697

    
698
  if kind.startswith("choices=") or kind.startswith("suggest="):
699
    (kind, choices) = kind.split("=", 1)
700
    kwargs["choices"] = choices.split(",")
701

    
702
  if kind not in _ARG_MAP:
703
    raise Exception("Unhandled argument kind '%s'" % kind)
704
  else:
705
    return _ARG_MAP[kind](**kwargs)
706

    
707

    
708
def WriteHaskellCompletion(sw, script, htools=True, debug=True):
709
  """Generates completion information for a Haskell program.
710

    
711
  This Converts completion info from a Haskell program into 'fake'
712
  cli_opts and then builds completion for them.
713

    
714
  """
715
  if htools:
716
    cmd = "./htools/htools"
717
    env = {"HTOOLS": script}
718
    script_name = script
719
    func_name = "htools_%s" % script
720
  else:
721
    # note: this is not yet used (daemons)
722
    cmd = script
723
    env = {}
724
    script_name = script
725
    func_name = script
726
  output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
727
  cli_opts = []
728
  args = []
729
  for line in output.splitlines():
730
    v = line.split(None)
731
    exc = lambda msg: Exception("Invalid %s output from %s: %s" %
732
                                (msg, script, v))
733
    if len(v) < 2:
734
      raise exc("help completion")
735
    if v[0].startswith("-"):
736
      if len(v) != 2:
737
        raise exc("option format")
738
      (opts, kind) = v
739
      cli_opts.append(HaskellOptToOptParse(opts, kind))
740
    else:
741
      if len(v) != 3:
742
        raise exc("argument format")
743
      (kind, min_cnt, max_cnt) = v
744
      args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
745
  WriteCompletion(sw, script_name, func_name, debug, opts=cli_opts, args=args)
746

    
747

    
748
def main():
749
  parser = optparse.OptionParser(usage="%prog [--compact]")
750
  parser.add_option("--compact", action="store_true",
751
                    help=("Don't indent output and don't include debugging"
752
                          " facilities"))
753

    
754
  options, args = parser.parse_args()
755
  if args:
756
    parser.error("Wrong number of arguments")
757

    
758
  buf = StringIO()
759
  sw = utils.ShellWriter(buf, indent=not options.compact)
760

    
761
  # Remember original state of extglob and enable it (required for pattern
762
  # matching; must be enabled while parsing script)
763
  sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
764
  sw.Write("shopt -s extglob")
765

    
766
  WritePreamble(sw, not options.compact)
767

    
768
  # gnt-* scripts
769
  for scriptname in _autoconf.GNT_SCRIPTS:
770
    filename = "scripts/%s" % scriptname
771

    
772
    WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
773
                    not options.compact,
774
                    commands=GetCommands(filename,
775
                                         build.LoadModule(filename)))
776

    
777
  # Burnin script
778
  burnin = build.LoadModule("tools/burnin")
779
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
780
                  not options.compact,
781
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
782

    
783
  # htools, if enabled
784
  if _autoconf.HTOOLS:
785
    for script in _autoconf.HTOOLS_PROGS:
786
      WriteHaskellCompletion(sw, script, htools=True,
787
                             debug=not options.compact)
788

    
789
  # Reset extglob to original value
790
  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
791
  sw.Write("unset gnt_shopt_extglob")
792

    
793
  print buf.getvalue()
794

    
795

    
796
if __name__ == "__main__":
797
  main()