Statistics
| Branch: | Tag: | Revision:

root / autotools / build-bash-completion @ 7ca67731

History | View | Annotate | Download (23.7 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
  # Params: <offset> <options with values> <options without values>
139
  # Result variable: $first_arg_idx
140
  sw.Write("_ganeti_find_first_arg() {")
141
  sw.IncIndent()
142
  try:
143
    sw.Write("local w i")
144

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

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

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

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

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

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

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

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

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

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

    
238

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

    
243

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
325
    cur = "\"$optcur\""
326

    
327
    wrote_opt = False
328

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

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

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

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

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

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

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

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

    
404
      wrote_opt = True
405

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

    
409
    return
410

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

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

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

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

    
430
      sw.Write("compgenargs=")
431

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

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

    
441
        compgenargs = []
442

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

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

    
479
        last_arg_end += arg.min
480

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

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

    
498
          wrote_arg = True
499

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

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

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

    
514

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

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

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

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

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

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

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

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

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

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

    
590

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

    
594

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

    
598
  Aliases are also added as commands.
599

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

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

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

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

    
627
  return commands
628

    
629

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

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

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

    
668

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

    
684

    
685
def HaskellArgToCliArg(kind, min_cnt, max_cnt):
686
  """Converts a Haskell options to Python _Argument.
687

    
688
  @type kind: string
689
  @param kind: type generated by Common.hs/argComplToText; needs to be
690
      kept in sync
691

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

    
702
  if kind.startswith("choices=") or kind.startswith("suggest="):
703
    (kind, choices) = kind.split("=", 1)
704
    kwargs["choices"] = choices.split(",")
705

    
706
  if kind not in _ARG_MAP:
707
    raise Exception("Unhandled argument kind '%s'" % kind)
708
  else:
709
    return _ARG_MAP[kind](**kwargs)
710

    
711

    
712
def WriteHaskellCompletion(sw, script, htools=True, debug=True):
713
  """Generates completion information for a Haskell program.
714

    
715
  This Converts completion info from a Haskell program into 'fake'
716
  cli_opts and then builds completion for them.
717

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

    
751

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

    
758
  options, args = parser.parse_args()
759
  if args:
760
    parser.error("Wrong number of arguments")
761

    
762
  buf = StringIO()
763
  sw = utils.ShellWriter(buf, indent=not options.compact)
764

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

    
770
  WritePreamble(sw, not options.compact)
771

    
772
  # gnt-* scripts
773
  for scriptname in _autoconf.GNT_SCRIPTS:
774
    filename = "scripts/%s" % scriptname
775

    
776
    WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
777
                    not options.compact,
778
                    commands=GetCommands(filename,
779
                                         build.LoadModule(filename)))
780

    
781
  # Burnin script
782
  burnin = build.LoadModule("tools/burnin")
783
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
784
                  not options.compact,
785
                  opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
786

    
787
  # ganeti-cleaner
788
  WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
789
                         debug=not options.compact)
790

    
791
  # htools, if enabled
792
  if _autoconf.HTOOLS:
793
    for script in _autoconf.HTOOLS_PROGS:
794
      WriteHaskellCompletion(sw, script, htools=True,
795
                             debug=not options.compact)
796

    
797
  # ganeti-confd, if enabled
798
  if _autoconf.ENABLE_CONFD:
799
    WriteHaskellCompletion(sw, "htools/ganeti-confd", htools=False,
800
                           debug=not options.compact)
801

    
802
  # Reset extglob to original value
803
  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
804
  sw.Write("unset gnt_shopt_extglob")
805

    
806
  print buf.getvalue()
807

    
808

    
809
if __name__ == "__main__":
810
  main()