Including missing RST files in packaging
[ganeti-local] / autotools / build-bash-completion
index ec9c0a6..63def12 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
 #!/usr/bin/python
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # [C0103] Invalid name build-bash-completion
 
 import os
 # [C0103] Invalid name build-bash-completion
 
 import os
+import os.path
 import re
 import re
+import itertools
+import optparse
 from cStringIO import StringIO
 
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
 from ganeti import build
 from cStringIO import StringIO
 
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
 from ganeti import build
+from ganeti import pathutils
+
+from ganeti.tools import burnin
 
 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
 # making an exception here because this script is only used at build time.
 from ganeti import _autoconf
 
 
 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
 # making an exception here because this script is only used at build time.
 from ganeti import _autoconf
 
+#: Regular expression describing desired format of option names. Long names can
+#: contain lowercase characters, numbers and dashes only.
+_OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
+
 
 
-def WritePreamble(sw):
+def WritePreamble(sw, support_debug):
   """Writes the script preamble.
 
   Helper functions should be written here.
   """Writes the script preamble.
 
   Helper functions should be written here.
@@ -49,32 +59,33 @@ def WritePreamble(sw):
   sw.Write("# This script is automatically generated at build time.")
   sw.Write("# Do not modify manually.")
 
   sw.Write("# This script is automatically generated at build time.")
   sw.Write("# Do not modify manually.")
 
-  sw.Write("_ganeti_dbglog() {")
-  sw.IncIndent()
-  try:
-    sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
+  if support_debug:
+    sw.Write("_gnt_log() {")
     sw.IncIndent()
     try:
     sw.IncIndent()
     try:
-      sw.Write("{")
+      sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
       sw.IncIndent()
       try:
       sw.IncIndent()
       try:
-        sw.Write("echo ---")
-        sw.Write("echo \"$@\"")
-        sw.Write("echo")
+        sw.Write("{")
+        sw.IncIndent()
+        try:
+          sw.Write("echo ---")
+          sw.Write("echo \"$@\"")
+          sw.Write("echo")
+        finally:
+          sw.DecIndent()
+        sw.Write("} >> $GANETI_COMPL_LOG")
       finally:
         sw.DecIndent()
       finally:
         sw.DecIndent()
-      sw.Write("} >> $GANETI_COMPL_LOG")
+      sw.Write("fi")
     finally:
       sw.DecIndent()
     finally:
       sw.DecIndent()
-    sw.Write("fi")
-  finally:
-    sw.DecIndent()
-  sw.Write("}")
+    sw.Write("}")
 
   sw.Write("_ganeti_nodes() {")
   sw.IncIndent()
   try:
 
   sw.Write("_ganeti_nodes() {")
   sw.IncIndent()
   try:
-    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
+    node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
   finally:
     sw.DecIndent()
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
   finally:
     sw.DecIndent()
@@ -83,7 +94,7 @@ def WritePreamble(sw):
   sw.Write("_ganeti_instances() {")
   sw.IncIndent()
   try:
   sw.Write("_ganeti_instances() {")
   sw.IncIndent()
   try:
-    instance_list_path = os.path.join(constants.DATA_DIR,
+    instance_list_path = os.path.join(pathutils.DATA_DIR,
                                       "ssconf_instance_list")
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
   finally:
                                       "ssconf_instance_list")
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
   finally:
@@ -94,18 +105,18 @@ def WritePreamble(sw):
   sw.IncIndent()
   try:
     # FIXME: this is really going into the internals of the job queue
   sw.IncIndent()
   try:
     # FIXME: this is really going into the internals of the job queue
-    sw.Write(("local jlist=$( shopt -s nullglob &&"
-              " cd %s 2>/dev/null && echo job-* || : )"),
-             utils.ShellQuote(constants.QUEUE_DIR))
-    sw.Write('echo "${jlist//job-/}"')
+    sw.Write(("local jlist=($( shopt -s nullglob &&"
+              " cd %s 2>/dev/null && echo job-* || : ))"),
+             utils.ShellQuote(pathutils.QUEUE_DIR))
+    sw.Write('echo "${jlist[@]/job-/}"')
   finally:
     sw.DecIndent()
   sw.Write("}")
 
   for (fnname, paths) in [
   finally:
     sw.DecIndent()
   sw.Write("}")
 
   for (fnname, paths) in [
-      ("os", constants.OS_SEARCH_PATH),
-      ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
-      ]:
+    ("os", pathutils.OS_SEARCH_PATH),
+    ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
+    ]:
     sw.Write("_ganeti_%s() {", fnname)
     sw.IncIndent()
     try:
     sw.Write("_ganeti_%s() {", fnname)
     sw.IncIndent()
     try:
@@ -120,12 +131,21 @@ def WritePreamble(sw):
   sw.Write("_ganeti_nodegroup() {")
   sw.IncIndent()
   try:
   sw.Write("_ganeti_nodegroup() {")
   sw.IncIndent()
   try:
-    nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
+    nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
   finally:
     sw.DecIndent()
   sw.Write("}")
 
     sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
   finally:
     sw.DecIndent()
   sw.Write("}")
 
+  sw.Write("_ganeti_network() {")
+  sw.IncIndent()
+  try:
+    networks_path = os.path.join(pathutils.DATA_DIR, "ssconf_networks")
+    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
   sw.Write("_ganeti_find_first_arg() {")
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
   sw.Write("_ganeti_find_first_arg() {")
@@ -187,7 +207,7 @@ def WritePreamble(sw):
 
   # Params: <long options with equal sign> <all options>
   # Result variable: $optcur
 
   # Params: <long options with equal sign> <all options>
   # Result variable: $optcur
-  sw.Write("_ganeti_checkopt() {")
+  sw.Write("_gnt_checkopt() {")
   sw.IncIndent()
   try:
     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
   sw.IncIndent()
   try:
     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
@@ -206,7 +226,8 @@ def WritePreamble(sw):
       sw.DecIndent()
     sw.Write("fi")
 
       sw.DecIndent()
     sw.Write("fi")
 
-    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
+    if support_debug:
+      sw.Write("_gnt_log optcur=\"'$optcur'\"")
 
     sw.Write("return 1")
   finally:
 
     sw.Write("return 1")
   finally:
@@ -215,18 +236,19 @@ def WritePreamble(sw):
 
   # Params: <compgen options>
   # Result variable: $COMPREPLY
 
   # Params: <compgen options>
   # Result variable: $COMPREPLY
-  sw.Write("_ganeti_compgen() {")
+  sw.Write("_gnt_compgen() {")
   sw.IncIndent()
   try:
     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
   sw.IncIndent()
   try:
     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
-    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
+    if support_debug:
+      sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
   finally:
     sw.DecIndent()
   sw.Write("}")
 
 
 def WriteCompReply(sw, args, cur="\"$cur\""):
   finally:
     sw.DecIndent()
   sw.Write("}")
 
 
 def WriteCompReply(sw, args, cur="\"$cur\""):
-  sw.Write("_ganeti_compgen %s -- %s", args, cur)
+  sw.Write("_gnt_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
   sw.Write("return")
 
 
@@ -234,16 +256,22 @@ class CompletionWriter:
   """Command completion writer class.
 
   """
   """Command completion writer class.
 
   """
-  def __init__(self, arg_offset, opts, args):
+  def __init__(self, arg_offset, opts, args, support_debug):
     self.arg_offset = arg_offset
     self.opts = opts
     self.args = args
     self.arg_offset = arg_offset
     self.opts = opts
     self.args = args
+    self.support_debug = support_debug
 
     for opt in opts:
       # While documented, these variables aren't seen as public attributes by
       # pylint. pylint: disable=W0212
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
 
     for opt in opts:
       # While documented, these variables aren't seen as public attributes by
       # pylint. pylint: disable=W0212
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
+      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
+      if invalid:
+        raise Exception("Option names don't match regular expression '%s': %s" %
+                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
+
   def _FindFirstArgument(self, sw):
     ignore = []
     skip_one = []
   def _FindFirstArgument(self, sw):
     ignore = []
     skip_one = []
@@ -309,7 +337,7 @@ class CompletionWriter:
 
     wrote_opt = False
 
 
     wrote_opt = False
 
-    for (suggest, allnames) in values.iteritems():
+    for (suggest, allnames) in values.items():
       longnames = [i for i in allnames if i.startswith("--")]
 
       if wrote_opt:
       longnames = [i for i in allnames if i.startswith("--")]
 
       if wrote_opt:
@@ -317,7 +345,7 @@ class CompletionWriter:
       else:
         condcmd = "if"
 
       else:
         condcmd = "if"
 
-      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
+      sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
                utils.ShellQuote("|".join(allnames)))
       sw.IncIndent()
                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
                utils.ShellQuote("|".join(allnames)))
       sw.IncIndent()
@@ -331,10 +359,14 @@ class CompletionWriter:
           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_OS:
           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_OS:
           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
+          WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
           WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
           WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_NETWORK:
+          WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
 
         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
 
@@ -354,8 +386,9 @@ class CompletionWriter:
             sw.DecIndent()
           sw.Write("fi")
 
             sw.DecIndent()
           sw.Write("fi")
 
-          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
-                   " node1=\"'$node1'\"")
+          if self.support_debug:
+            sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
+                     " node1=\"'$node1'\"")
 
           sw.Write("for i in $(_ganeti_nodes); do")
           sw.IncIndent()
 
           sw.Write("for i in $(_ganeti_nodes); do")
           sw.IncIndent()
@@ -409,10 +442,6 @@ class CompletionWriter:
       varlen_arg_idx = None
       wrote_arg = False
 
       varlen_arg_idx = None
       wrote_arg = False
 
-      # Write some debug comments
-      for idx, arg in enumerate(self.args):
-        sw.Write("# %s: %r", idx, arg)
-
       sw.Write("compgenargs=")
 
       for idx, arg in enumerate(self.args):
       sw.Write("compgenargs=")
 
       for idx, arg in enumerate(self.args):
@@ -436,10 +465,14 @@ class CompletionWriter:
           choices = "$(_ganeti_nodes)"
         elif isinstance(arg, cli.ArgGroup):
           choices = "$(_ganeti_nodegroup)"
           choices = "$(_ganeti_nodes)"
         elif isinstance(arg, cli.ArgGroup):
           choices = "$(_ganeti_nodegroup)"
+        elif isinstance(arg, cli.ArgNetwork):
+          choices = "$(_ganeti_network)"
         elif isinstance(arg, cli.ArgJobId):
           choices = "$(_ganeti_jobs)"
         elif isinstance(arg, cli.ArgOs):
           choices = "$(_ganeti_os)"
         elif isinstance(arg, cli.ArgJobId):
           choices = "$(_ganeti_jobs)"
         elif isinstance(arg, cli.ArgOs):
           choices = "$(_ganeti_os)"
+        elif isinstance(arg, cli.ArgExtStorage):
+          choices = "$(_ganeti_extstorage)"
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
@@ -498,7 +531,7 @@ class CompletionWriter:
     self._CompleteArguments(sw)
 
 
     self._CompleteArguments(sw)
 
 
-def WriteCompletion(sw, scriptname, funcname,
+def WriteCompletion(sw, scriptname, funcname, support_debug,
                     commands=None,
                     opts=None, args=None):
   """Writes the completion code for one command.
                     commands=None,
                     opts=None, args=None):
   """Writes the completion code for one command.
@@ -521,15 +554,16 @@ def WriteCompletion(sw, scriptname, funcname,
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
              ' i first_arg_idx choices compgenargs arg_idx optcur')
 
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
              ' i first_arg_idx choices compgenargs arg_idx optcur')
 
-    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
-    sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
-             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
+    if support_debug:
+      sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
+      sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
+               " _gnt_log \"$(set | grep ^COMP_)\"")
 
     sw.Write("COMPREPLY=()")
 
     if opts is not None and args is not None:
       assert not commands
 
     sw.Write("COMPREPLY=()")
 
     if opts is not None and args is not None:
       assert not commands
-      CompletionWriter(0, opts, args).WriteTo(sw)
+      CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
 
     else:
       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
 
     else:
       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
@@ -543,20 +577,25 @@ def WriteCompletion(sw, scriptname, funcname,
         sw.DecIndent()
       sw.Write("fi")
 
         sw.DecIndent()
       sw.Write("fi")
 
-      # We're doing options and arguments to commands
-      sw.Write("""case "${COMP_WORDS[1]}" in""")
-      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
+      # Group commands by arguments and options
+      grouped_cmds = {}
+      for cmd, (_, argdef, optdef, _, _) in commands.items():
         if not (argdef or optdef):
           continue
         if not (argdef or optdef):
           continue
+        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
 
 
-        # TODO: Group by arguments and options
-        sw.Write("%s)", utils.ShellQuote(cmd))
+      # We're doing options and arguments to commands
+      sw.Write("""case "${COMP_WORDS[1]}" in""")
+      sort_grouped = sorted(grouped_cmds.items(),
+                            key=lambda (_, y): sorted(y)[0])
+      for ((argdef, optdef), cmds) in sort_grouped:
+        assert argdef or optdef
+        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
         sw.IncIndent()
         try:
         sw.IncIndent()
         try:
-          CompletionWriter(1, optdef, argdef).WriteTo(sw)
+          CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
         finally:
           sw.DecIndent()
         finally:
           sw.DecIndent()
-
         sw.Write(";;")
       sw.Write("esac")
   finally:
         sw.Write(";;")
       sw.Write("esac")
   finally:
@@ -601,32 +640,232 @@ def GetCommands(filename, module):
   aliases = getattr(module, "aliases", {})
   if aliases:
     commands = commands.copy()
   aliases = getattr(module, "aliases", {})
   if aliases:
     commands = commands.copy()
-    for name, target in aliases.iteritems():
+    for name, target in aliases.items():
       commands[name] = commands[target]
 
   return commands
 
 
       commands[name] = commands[target]
 
   return commands
 
 
+def HaskellOptToOptParse(opts, kind):
+  """Converts a Haskell options to Python cli_options.
+
+  @type opts: string
+  @param opts: comma-separated string with short and long options
+  @type kind: string
+  @param kind: type generated by Common.hs/complToText; needs to be
+      kept in sync
+
+  """
+  # pylint: disable=W0142
+  # since we pass *opts in a number of places
+  opts = opts.split(",")
+  if kind == "none":
+    return cli.cli_option(*opts, action="store_true")
+  elif kind in ["file", "string", "host", "dir", "inetaddr"]:
+    return cli.cli_option(*opts, type="string")
+  elif kind == "integer":
+    return cli.cli_option(*opts, type="int")
+  elif kind == "float":
+    return cli.cli_option(*opts, type="float")
+  elif kind == "onegroup":
+    return cli.cli_option(*opts, type="string",
+                           completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
+  elif kind == "onenode":
+    return cli.cli_option(*opts, type="string",
+                          completion_suggest=cli.OPT_COMPL_ONE_NODE)
+  elif kind == "manyinstances":
+    # FIXME: no support for many instances
+    return cli.cli_option(*opts, type="string")
+  elif kind.startswith("choices="):
+    choices = kind[len("choices="):].split(",")
+    return cli.cli_option(*opts, type="choice", choices=choices)
+  else:
+    # FIXME: there are many other currently unused completion types,
+    # should be added on an as-needed basis
+    raise Exception("Unhandled option kind '%s'" % kind)
+
+
+#: serialised kind to arg type
+_ARG_MAP = {
+  "choices": cli.ArgChoice,
+  "command": cli.ArgCommand,
+  "file": cli.ArgFile,
+  "host": cli.ArgHost,
+  "jobid": cli.ArgJobId,
+  "onegroup": cli.ArgGroup,
+  "oneinstance": cli.ArgInstance,
+  "onenode": cli.ArgNode,
+  "oneos": cli.ArgOs,
+  "string": cli.ArgUnknown,
+  "suggests": cli.ArgSuggest,
+  }
+
+
+def HaskellArgToCliArg(kind, min_cnt, max_cnt):
+  """Converts a Haskell options to Python _Argument.
+
+  @type kind: string
+  @param kind: type generated by Common.hs/argComplToText; needs to be
+      kept in sync
+
+  """
+  min_cnt = int(min_cnt)
+  if max_cnt == "none":
+    max_cnt = None
+  else:
+    max_cnt = int(max_cnt)
+  # pylint: disable=W0142
+  # since we pass **kwargs
+  kwargs = {"min": min_cnt, "max": max_cnt}
+
+  if kind.startswith("choices=") or kind.startswith("suggest="):
+    (kind, choices) = kind.split("=", 1)
+    kwargs["choices"] = choices.split(",")
+
+  if kind not in _ARG_MAP:
+    raise Exception("Unhandled argument kind '%s'" % kind)
+  else:
+    return _ARG_MAP[kind](**kwargs)
+
+
+def ParseHaskellOptsArgs(script, output):
+  """Computes list of options/arguments from help-completion output.
+
+  """
+  cli_opts = []
+  cli_args = []
+  for line in output.splitlines():
+    v = line.split(None)
+    exc = lambda msg: Exception("Invalid %s output from %s: %s" %
+                                (msg, script, v))
+    if len(v) < 2:
+      raise exc("help completion")
+    if v[0].startswith("-"):
+      if len(v) != 2:
+        raise exc("option format")
+      (opts, kind) = v
+      cli_opts.append(HaskellOptToOptParse(opts, kind))
+    else:
+      if len(v) != 3:
+        raise exc("argument format")
+      (kind, min_cnt, max_cnt) = v
+      cli_args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
+  return (cli_opts, cli_args)
+
+
+def WriteHaskellCompletion(sw, script, htools=True, debug=True):
+  """Generates completion information for a Haskell program.
+
+  This converts completion info from a Haskell program into 'fake'
+  cli_opts and then builds completion for them.
+
+  """
+  if htools:
+    cmd = "./src/htools"
+    env = {"HTOOLS": script}
+    script_name = script
+    func_name = "htools_%s" % script
+  else:
+    cmd = "./" + script
+    env = {}
+    script_name = os.path.basename(script)
+    func_name = script_name
+  func_name = GetFunctionName(func_name)
+  output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
+  (opts, args) = ParseHaskellOptsArgs(script_name, output)
+  WriteCompletion(sw, script_name, func_name, debug, opts=opts, args=args)
+
+
+def WriteHaskellCmdCompletion(sw, script, debug=True):
+  """Generates completion information for a Haskell multi-command program.
+
+  This gathers the list of commands from a Haskell program and
+  computes the list of commands available, then builds the sub-command
+  list of options/arguments for each command, using that for building
+  a unified help output.
+
+  """
+  cmd = "./" + script
+  script_name = os.path.basename(script)
+  func_name = script_name
+  func_name = GetFunctionName(func_name)
+  output = utils.RunCmd([cmd, "--help-completion"], cwd=".").output
+  commands = {}
+  lines = output.splitlines()
+  if len(lines) != 1:
+    raise Exception("Invalid lines in multi-command mode: %s" % str(lines))
+  v = lines[0].split(None)
+  exc = lambda msg: Exception("Invalid %s output from %s: %s" %
+                              (msg, script, v))
+  if len(v) != 3:
+    raise exc("help completion in multi-command mode")
+  if not v[0].startswith("choices="):
+    raise exc("invalid format in multi-command mode '%s'" % v[0])
+  for subcmd in v[0][len("choices="):].split(","):
+    output = utils.RunCmd([cmd, subcmd, "--help-completion"], cwd=".").output
+    (opts, args) = ParseHaskellOptsArgs(script, output)
+    commands[subcmd] = (None, args, opts, None, None)
+  WriteCompletion(sw, script_name, func_name, debug, commands=commands)
+
+
 def main():
 def main():
+  parser = optparse.OptionParser(usage="%prog [--compact]")
+  parser.add_option("--compact", action="store_true",
+                    help=("Don't indent output and don't include debugging"
+                          " facilities"))
+
+  options, args = parser.parse_args()
+  if args:
+    parser.error("Wrong number of arguments")
+
+  # Whether to build debug version of completion script
+  debug = not options.compact
+
   buf = StringIO()
   buf = StringIO()
-  sw = utils.ShellWriter(buf)
+  sw = utils.ShellWriter(buf, indent=debug)
+
+  # Remember original state of extglob and enable it (required for pattern
+  # matching; must be enabled while parsing script)
+  sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
+  sw.Write("shopt -s extglob")
 
 
-  WritePreamble(sw)
+  WritePreamble(sw, debug)
 
   # gnt-* scripts
   for scriptname in _autoconf.GNT_SCRIPTS:
     filename = "scripts/%s" % scriptname
 
 
   # gnt-* scripts
   for scriptname in _autoconf.GNT_SCRIPTS:
     filename = "scripts/%s" % scriptname
 
-    WriteCompletion(sw, scriptname,
-                    GetFunctionName(scriptname),
+    WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
                     commands=GetCommands(filename,
                                          build.LoadModule(filename)))
 
   # Burnin script
                     commands=GetCommands(filename,
                                          build.LoadModule(filename)))
 
   # Burnin script
-  burnin = build.LoadModule("tools/burnin")
-  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
+  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
+                  debug,
                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
 
                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
 
+  # ganeti-cleaner
+  WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
+                         debug=not options.compact)
+
+  # htools, if enabled
+  if _autoconf.HTOOLS:
+    for script in _autoconf.HTOOLS_PROGS:
+      WriteHaskellCompletion(sw, script, htools=True, debug=debug)
+
+  # ganeti-confd, if enabled
+  if _autoconf.ENABLE_CONFD:
+    WriteHaskellCompletion(sw, "src/ganeti-confd", htools=False,
+                           debug=debug)
+
+  # mon-collector, if monitoring is enabled
+  if _autoconf.ENABLE_MOND:
+    WriteHaskellCmdCompletion(sw, "src/mon-collector", debug=debug)
+
+  # Reset extglob to original value
+  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
+  sw.Write("unset gnt_shopt_extglob")
+
   print buf.getvalue()
 
 
   print buf.getvalue()