Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ b954f097

History | View | Annotate | Download (26 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_EXTSTORAGE:
361
          WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
362
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
363
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
364
        elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
365
          WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
366
        elif suggest == cli.OPT_COMPL_ONE_NETWORK:
367
          WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
368
        elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
369
          sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
370

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

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

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

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

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

    
417
      wrote_opt = True
418

    
419
    if wrote_opt:
420
      sw.Write("fi")
421

    
422
    return
423

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

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

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

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

    
443
      sw.Write("compgenargs=")
444

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

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

    
454
        compgenargs = []
455

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

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

    
496
        last_arg_end += arg.min
497

    
498
        if choices or compgenargs:
499
          if wrote_arg:
500
            condcmd = "elif"
501
          else:
502
            condcmd = "if"
503

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

    
515
          wrote_arg = True
516

    
517
      if wrote_arg:
518
        sw.Write("fi")
519

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

    
526
  def WriteTo(self, sw):
527
    self._FindFirstArgument(sw)
528
    self._CompleteOptionValues(sw)
529
    self._CompleteArguments(sw)
530

    
531

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

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

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

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

    
560
    sw.Write("COMPREPLY=()")
561

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

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

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

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

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

    
607

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

    
611

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

    
615
  Aliases are also added as commands.
616

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

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

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

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

    
644
  return commands
645

    
646

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

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

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

    
685

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

    
701

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

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

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

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

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

    
728

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

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

    
753

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

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

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

    
776

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

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

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

    
808

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

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

    
819
  # Whether to build debug version of completion script
820
  debug = not options.compact
821

    
822
  buf = StringIO()
823
  sw = utils.ShellWriter(buf, indent=debug)
824

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

    
830
  WritePreamble(sw, debug)
831

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

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

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

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

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

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

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

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

    
868
  print buf.getvalue()
869

    
870

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