Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ ea2bcb82

History | View | Annotate | Download (25.9 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
from ganeti.tools import burnin
43

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

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

    
52

    
53
def WritePreamble(sw, support_debug):
54
  """Writes the script preamble.
55

    
56
  Helper functions should be written here.
57

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
249

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

    
254

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

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

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

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

    
275
  def _FindFirstArgument(self, sw):
276
    ignore = []
277
    skip_one = []
278

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

    
289
    ignore = sorted(utils.UniqueSequence(ignore))
290
    skip_one = sorted(utils.UniqueSequence(skip_one))
291

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

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

    
308
    for opt in self.opts:
309
      if not opt.takes_value():
310
        continue
311

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

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

    
319
      if not suggest:
320
        suggest = opt.choices
321

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

    
330
      values.setdefault(key, []).extend(opt.all_names)
331

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

    
336
    cur = "\"$optcur\""
337

    
338
    wrote_opt = False
339

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

    
343
      if wrote_opt:
344
        condcmd = "elif"
345
      else:
346
        condcmd = "if"
347

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

    
373
          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
374
          sw.IncIndent()
375
          try:
376
            sw.Write("node1=\"${optcur%%:*}\"")
377

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

    
389
          if self.support_debug:
390
            sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
391
                     " node1=\"'$node1'\"")
392

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

    
413
          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
414
        else:
415
          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
416
      finally:
417
        sw.DecIndent()
418

    
419
      wrote_opt = True
420

    
421
    if wrote_opt:
422
      sw.Write("fi")
423

    
424
    return
425

    
426
  def _CompleteArguments(self, sw):
427
    if not (self.opts or self.args):
428
      return
429

    
430
    all_option_names = []
431
    for opt in self.opts:
432
      all_option_names.extend(opt.all_names)
433
    all_option_names.sort()
434

    
435
    # List options if no argument has been specified yet
436
    sw.Write("_ganeti_list_options %s",
437
             utils.ShellQuote(" ".join(all_option_names)))
438

    
439
    if self.args:
440
      last_idx = len(self.args) - 1
441
      last_arg_end = 0
442
      varlen_arg_idx = None
443
      wrote_arg = False
444

    
445
      sw.Write("compgenargs=")
446

    
447
      for idx, arg in enumerate(self.args):
448
        assert arg.min is not None and arg.min >= 0
449
        assert not (idx < last_idx and arg.max is None)
450

    
451
        if arg.min != arg.max or arg.max is None:
452
          if varlen_arg_idx is not None:
453
            raise Exception("Only one argument can have a variable length")
454
          varlen_arg_idx = idx
455

    
456
        compgenargs = []
457

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

    
488
        if arg.min == 1 and arg.max == 1:
489
          cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
490
        elif arg.max is None:
491
          cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
492
        elif arg.min <= arg.max:
493
          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
494
                     (last_arg_end, last_arg_end + arg.max))
495
        else:
496
          raise Exception("Unable to generate argument position condition")
497

    
498
        last_arg_end += arg.min
499

    
500
        if choices or compgenargs:
501
          if wrote_arg:
502
            condcmd = "elif"
503
          else:
504
            condcmd = "if"
505

    
506
          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
507
          sw.IncIndent()
508
          try:
509
            if choices:
510
              sw.Write("""choices="$choices "%s""", choices)
511
            if compgenargs:
512
              sw.Write("compgenargs=%s",
513
                       utils.ShellQuote(" ".join(compgenargs)))
514
          finally:
515
            sw.DecIndent()
516

    
517
          wrote_arg = True
518

    
519
      if wrote_arg:
520
        sw.Write("fi")
521

    
522
    if self.args:
523
      WriteCompReply(sw, """-W "$choices" $compgenargs""")
524
    else:
525
      # $compgenargs exists only if there are arguments
526
      WriteCompReply(sw, '-W "$choices"')
527

    
528
  def WriteTo(self, sw):
529
    self._FindFirstArgument(sw)
530
    self._CompleteOptionValues(sw)
531
    self._CompleteArguments(sw)
532

    
533

    
534
def WriteCompletion(sw, scriptname, funcname, support_debug,
535
                    commands=None,
536
                    opts=None, args=None):
537
  """Writes the completion code for one command.
538

    
539
  @type sw: ShellWriter
540
  @param sw: Script writer
541
  @type scriptname: string
542
  @param scriptname: Name of command line program
543
  @type funcname: string
544
  @param funcname: Shell function name
545
  @type commands: list
546
  @param commands: List of all subcommands in this program
547

    
548
  """
549
  sw.Write("%s() {", funcname)
550
  sw.IncIndent()
551
  try:
552
    sw.Write("local "
553
             ' cur="${COMP_WORDS[COMP_CWORD]}"'
554
             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
555
             ' i first_arg_idx choices compgenargs arg_idx optcur')
556

    
557
    if support_debug:
558
      sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
559
      sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
560
               " _gnt_log \"$(set | grep ^COMP_)\"")
561

    
562
    sw.Write("COMPREPLY=()")
563

    
564
    if opts is not None and args is not None:
565
      assert not commands
566
      CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
567

    
568
    else:
569
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
570
      sw.IncIndent()
571
      try:
572
        # Complete the command name
573
        WriteCompReply(sw,
574
                       ("-W %s" %
575
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
576
      finally:
577
        sw.DecIndent()
578
      sw.Write("fi")
579

    
580
      # Group commands by arguments and options
581
      grouped_cmds = {}
582
      for cmd, (_, argdef, optdef, _, _) in commands.items():
583
        if not (argdef or optdef):
584
          continue
585
        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
586

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

    
605
  sw.Write("complete -F %s -o filenames %s",
606
           utils.ShellQuote(funcname),
607
           utils.ShellQuote(scriptname))
608

    
609

    
610
def GetFunctionName(name):
611
  return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
612

    
613

    
614
def GetCommands(filename, module):
615
  """Returns the commands defined in a module.
616

    
617
  Aliases are also added as commands.
618

    
619
  """
620
  try:
621
    commands = getattr(module, "commands")
622
  except AttributeError:
623
    raise Exception("Script %s doesn't have 'commands' attribute" %
624
                    filename)
625

    
626
  # Add the implicit "--help" option
627
  help_option = cli.cli_option("-h", "--help", default=False,
628
                               action="store_true")
629

    
630
  for name, (_, _, optdef, _, _) in commands.items():
631
    if help_option not in optdef:
632
      optdef.append(help_option)
633
    for opt in cli.COMMON_OPTS:
634
      if opt in optdef:
635
        raise Exception("Common option '%s' listed for command '%s' in %s" %
636
                        (opt, name, filename))
637
      optdef.append(opt)
638

    
639
  # Use aliases
640
  aliases = getattr(module, "aliases", {})
641
  if aliases:
642
    commands = commands.copy()
643
    for name, target in aliases.items():
644
      commands[name] = commands[target]
645

    
646
  return commands
647

    
648

    
649
def HaskellOptToOptParse(opts, kind):
650
  """Converts a Haskell options to Python cli_options.
651

    
652
  @type opts: string
653
  @param opts: comma-separated string with short and long options
654
  @type kind: string
655
  @param kind: type generated by Common.hs/complToText; needs to be
656
      kept in sync
657

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

    
687

    
688
#: serialised kind to arg type
689
_ARG_MAP = {
690
  "choices": cli.ArgChoice,
691
  "command": cli.ArgCommand,
692
  "file": cli.ArgFile,
693
  "host": cli.ArgHost,
694
  "jobid": cli.ArgJobId,
695
  "onegroup": cli.ArgGroup,
696
  "oneinstance": cli.ArgInstance,
697
  "onenode": cli.ArgNode,
698
  "oneos": cli.ArgOs,
699
  "string": cli.ArgUnknown,
700
  "suggests": cli.ArgSuggest,
701
  }
702

    
703

    
704
def HaskellArgToCliArg(kind, min_cnt, max_cnt):
705
  """Converts a Haskell options to Python _Argument.
706

    
707
  @type kind: string
708
  @param kind: type generated by Common.hs/argComplToText; needs to be
709
      kept in sync
710

    
711
  """
712
  min_cnt = int(min_cnt)
713
  if max_cnt == "none":
714
    max_cnt = None
715
  else:
716
    max_cnt = int(max_cnt)
717
  # pylint: disable=W0142
718
  # since we pass **kwargs
719
  kwargs = {"min": min_cnt, "max": max_cnt}
720

    
721
  if kind.startswith("choices=") or kind.startswith("suggest="):
722
    (kind, choices) = kind.split("=", 1)
723
    kwargs["choices"] = choices.split(",")
724

    
725
  if kind not in _ARG_MAP:
726
    raise Exception("Unhandled argument kind '%s'" % kind)
727
  else:
728
    return _ARG_MAP[kind](**kwargs)
729

    
730

    
731
def ParseHaskellOptsArgs(script, output):
732
  """Computes list of options/arguments from help-completion output.
733

    
734
  """
735
  cli_opts = []
736
  cli_args = []
737
  for line in output.splitlines():
738
    v = line.split(None)
739
    exc = lambda msg: Exception("Invalid %s output from %s: %s" %
740
                                (msg, script, v))
741
    if len(v) < 2:
742
      raise exc("help completion")
743
    if v[0].startswith("-"):
744
      if len(v) != 2:
745
        raise exc("option format")
746
      (opts, kind) = v
747
      cli_opts.append(HaskellOptToOptParse(opts, kind))
748
    else:
749
      if len(v) != 3:
750
        raise exc("argument format")
751
      (kind, min_cnt, max_cnt) = v
752
      cli_args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
753
  return (cli_opts, cli_args)
754

    
755

    
756
def WriteHaskellCompletion(sw, script, htools=True, debug=True):
757
  """Generates completion information for a Haskell program.
758

    
759
  This converts completion info from a Haskell program into 'fake'
760
  cli_opts and then builds completion for them.
761

    
762
  """
763
  if htools:
764
    cmd = "./src/htools"
765
    env = {"HTOOLS": script}
766
    script_name = script
767
    func_name = "htools_%s" % script
768
  else:
769
    cmd = "./" + script
770
    env = {}
771
    script_name = os.path.basename(script)
772
    func_name = script_name
773
  func_name = GetFunctionName(func_name)
774
  output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
775
  (opts, args) = ParseHaskellOptsArgs(script_name, output)
776
  WriteCompletion(sw, script_name, func_name, debug, opts=opts, args=args)
777

    
778

    
779
def WriteHaskellCmdCompletion(sw, script, debug=True):
780
  """Generates completion information for a Haskell multi-command program.
781

    
782
  This gathers the list of commands from a Haskell program and
783
  computes the list of commands available, then builds the sub-command
784
  list of options/arguments for each command, using that for building
785
  a unified help output.
786

    
787
  """
788
  cmd = "./" + script
789
  script_name = os.path.basename(script)
790
  func_name = script_name
791
  func_name = GetFunctionName(func_name)
792
  output = utils.RunCmd([cmd, "--help-completion"], cwd=".").output
793
  commands = {}
794
  lines = output.splitlines()
795
  if len(lines) != 1:
796
    raise Exception("Invalid lines in multi-command mode: %s" % str(lines))
797
  v = lines[0].split(None)
798
  exc = lambda msg: Exception("Invalid %s output from %s: %s" %
799
                              (msg, script, v))
800
  if len(v) != 3:
801
    raise exc("help completion in multi-command mode")
802
  if not v[0].startswith("choices="):
803
    raise exc("invalid format in multi-command mode '%s'" % v[0])
804
  for subcmd in v[0][len("choices="):].split(","):
805
    output = utils.RunCmd([cmd, subcmd, "--help-completion"], cwd=".").output
806
    (opts, args) = ParseHaskellOptsArgs(script, output)
807
    commands[subcmd] = (None, args, opts, None, None)
808
  WriteCompletion(sw, script_name, func_name, debug, commands=commands)
809

    
810

    
811
def main():
812
  parser = optparse.OptionParser(usage="%prog [--compact]")
813
  parser.add_option("--compact", action="store_true",
814
                    help=("Don't indent output and don't include debugging"
815
                          " facilities"))
816

    
817
  options, args = parser.parse_args()
818
  if args:
819
    parser.error("Wrong number of arguments")
820

    
821
  # Whether to build debug version of completion script
822
  debug = not options.compact
823

    
824
  buf = StringIO()
825
  sw = utils.ShellWriter(buf, indent=debug)
826

    
827
  # Remember original state of extglob and enable it (required for pattern
828
  # matching; must be enabled while parsing script)
829
  sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
830
  sw.Write("shopt -s extglob")
831

    
832
  WritePreamble(sw, debug)
833

    
834
  # gnt-* scripts
835
  for scriptname in _autoconf.GNT_SCRIPTS:
836
    filename = "scripts/%s" % scriptname
837

    
838
    WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
839
                    commands=GetCommands(filename,
840
                                         build.LoadModule(filename)))
841

    
842
  # Burnin script
843
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
844
                  debug,
845
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
846

    
847
  # ganeti-cleaner
848
  WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
849
                         debug=not options.compact)
850

    
851
  # htools, if enabled
852
  if _autoconf.HTOOLS:
853
    for script in _autoconf.HTOOLS_PROGS:
854
      WriteHaskellCompletion(sw, script, htools=True, debug=debug)
855

    
856
  # ganeti-confd, if enabled
857
  if _autoconf.ENABLE_CONFD:
858
    WriteHaskellCompletion(sw, "src/ganeti-confd", htools=False,
859
                           debug=debug)
860

    
861
  # mon-collector, if monitoring is enabled
862
  if _autoconf.ENABLE_MONITORING:
863
    WriteHaskellCmdCompletion(sw, "src/mon-collector", debug=debug)
864

    
865
  # Reset extglob to original value
866
  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
867
  sw.Write("unset gnt_shopt_extglob")
868

    
869
  print buf.getvalue()
870

    
871

    
872
if __name__ == "__main__":
873
  main()