Locking related fixes for networks
[ganeti-local] / autotools / build-bash-completion
index befbe6b..c484300 100755 (executable)
@@ -1,7 +1,7 @@
 #!/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
 # 02110-1301, USA.
 
 
-import imp
-import optparse
+"""Script to generate bash_completion script for Ganeti.
+
+"""
+
+# pylint: disable=C0103
+# [C0103] Invalid name build-bash-completion
+
 import os
-import sys
+import os.path
 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 ganeti import pathutils
 
 # _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]+$")
 
-class ShellWriter:
-  """Helper class to write scripts with indentation.
-
-  """
-  INDENT_STR = "  "
-
-  def __init__(self, fh):
-    self._fh = fh
-    self._indent = 0
-
-  def IncIndent(self):
-    """Increase indentation level by 1.
-
-    """
-    self._indent += 1
 
-  def DecIndent(self):
-    """Decrease indentation level by 1.
-
-    """
-    assert self._indent > 0
-    self._indent -= 1
-
-  def Write(self, txt, *args):
-    """Write line to output file.
-
-    """
-    self._fh.write(self._indent * self.INDENT_STR)
-
-    if args:
-      self._fh.write(txt % args)
-    else:
-      self._fh.write(txt)
-
-    self._fh.write("\n")
-
-
-def WritePreamble(sw):
+def WritePreamble(sw, support_debug):
   """Writes the script preamble.
 
   Helper functions should be written here.
@@ -81,11 +57,34 @@ def WritePreamble(sw):
   sw.Write("# This script is automatically generated at build time.")
   sw.Write("# Do not modify manually.")
 
+  if support_debug:
+    sw.Write("_gnt_log() {")
+    sw.IncIndent()
+    try:
+      sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
+      sw.IncIndent()
+      try:
+        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()
+      sw.Write("fi")
+    finally:
+      sw.DecIndent()
+    sw.Write("}")
+
   sw.Write("_ganeti_nodes() {")
   sw.IncIndent()
   try:
-    node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
-    sw.Write("cat %s", utils.ShellQuote(node_list_path))
+    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("}")
@@ -93,9 +92,9 @@ def WritePreamble(sw):
   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", utils.ShellQuote(instance_list_path))
+    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
   finally:
     sw.DecIndent()
   sw.Write("}")
@@ -104,19 +103,43 @@ def WritePreamble(sw):
   sw.IncIndent()
   try:
     # FIXME: this is really going into the internals of the job queue
-    sw.Write("local jlist=$( cd %s && 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("}")
 
-  sw.Write("_ganeti_os() {")
+  for (fnname, paths) in [
+    ("os", pathutils.OS_SEARCH_PATH),
+    ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
+    ]:
+    sw.Write("_ganeti_%s() {", fnname)
+    sw.IncIndent()
+    try:
+      # FIXME: Make querying the master for all OSes cheap
+      for path in paths:
+        sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
+                 utils.ShellQuote(path))
+    finally:
+      sw.DecIndent()
+    sw.Write("}")
+
+  sw.Write("_ganeti_nodegroup() {")
   sw.IncIndent()
   try:
-    # FIXME: Make querying the master for all OSes cheap
-    for path in constants.OS_SEARCH_PATH:
-      sw.Write("( cd %s && echo *; )", utils.ShellQuote(path))
+    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("_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("}")
@@ -180,9 +203,50 @@ def WritePreamble(sw):
     sw.DecIndent()
   sw.Write("}")
 
+  # Params: <long options with equal sign> <all options>
+  # Result variable: $optcur
+  sw.Write("_gnt_checkopt() {")
+  sw.IncIndent()
+  try:
+    sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
+    sw.IncIndent()
+    try:
+      sw.Write("optcur=\"${cur#--*=}\"")
+      sw.Write("return 0")
+    finally:
+      sw.DecIndent()
+    sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
+    sw.IncIndent()
+    try:
+      sw.Write("optcur=\"$cur\"")
+      sw.Write("return 0")
+    finally:
+      sw.DecIndent()
+    sw.Write("fi")
+
+    if support_debug:
+      sw.Write("_gnt_log optcur=\"'$optcur'\"")
+
+    sw.Write("return 1")
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
+  # Params: <compgen options>
+  # Result variable: $COMPREPLY
+  sw.Write("_gnt_compgen() {")
+  sw.IncIndent()
+  try:
+    sw.Write("""COMPREPLY=( $(compgen "$@") )""")
+    if support_debug:
+      sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
 
-def WriteCompReply(sw, args):
-  sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args)
+def WriteCompReply(sw, args, cur="\"$cur\""):
+  sw.Write("_gnt_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
@@ -190,14 +254,22 @@ class CompletionWriter:
   """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.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)
 
+      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 = []
@@ -206,7 +278,8 @@ class CompletionWriter:
       if opt.takes_value():
         # Ignore value
         for i in opt.all_names:
-          ignore.append("%s=*" % utils.ShellQuote(i))
+          if i.startswith("--"):
+            ignore.append("%s=*" % utils.ShellQuote(i))
           skip_one.append(utils.ShellQuote(i))
       else:
         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
@@ -237,37 +310,114 @@ class CompletionWriter:
       # Only static choices implemented so far (e.g. no node list)
       suggest = getattr(opt, "completion_suggest", None)
 
+      # our custom option type
+      if opt.type == "bool":
+        suggest = ["yes", "no"]
+
       if not suggest:
         suggest = opt.choices
 
-      if suggest:
-        suggest_text = " ".join(sorted(suggest))
+      if (isinstance(suggest, (int, long)) and
+          suggest in cli.OPT_COMPL_ALL):
+        key = suggest
+      elif suggest:
+        key = " ".join(sorted(suggest))
       else:
-        suggest_text = ""
+        key = ""
 
-      values.setdefault(suggest_text, []).extend(opt.all_names)
+      values.setdefault(key, []).extend(opt.all_names)
 
     # Don't write any code if there are no option values
     if not values:
       return
 
-    sw.Write("if [[ $COMP_CWORD -gt %s ]]; then", self.arg_offset + 1)
-    sw.IncIndent()
-    try:
-      sw.Write("""case "$prev" in""")
-      for (choices, names) in values.iteritems():
-        # TODO: Implement completion for --foo=bar form
-        sw.Write("%s)", "|".join([utils.ShellQuote(i) for i in names]))
-        sw.IncIndent()
-        try:
-          WriteCompReply(sw, "-W %s" % utils.ShellQuote(choices))
-        finally:
-          sw.DecIndent()
-        sw.Write(";;")
-      sw.Write("""esac""")
-    finally:
-      sw.DecIndent()
-    sw.Write("""fi""")
+    cur = "\"$optcur\""
+
+    wrote_opt = False
+
+    for (suggest, allnames) in values.items():
+      longnames = [i for i in allnames if i.startswith("--")]
+
+      if wrote_opt:
+        condcmd = "elif"
+      else:
+        condcmd = "if"
+
+      sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
+               utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
+               utils.ShellQuote("|".join(allnames)))
+      sw.IncIndent()
+      try:
+        if suggest == cli.OPT_COMPL_MANY_NODES:
+          # TODO: Implement comma-separated values
+          WriteCompReply(sw, "-W ''", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_NODE:
+          WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
+          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_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#*:}\"")
+
+          sw.Write("if [[ \"$optcur\" == *:* ]]; then")
+          sw.IncIndent()
+          try:
+            sw.Write("node1=\"${optcur%%:*}\"")
+
+            sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("pfx=\"$node1:\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("fi")
+          finally:
+            sw.DecIndent()
+          sw.Write("fi")
+
+          if self.support_debug:
+            sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
+                     " node1=\"'$node1'\"")
+
+          sw.Write("for i in $(_ganeti_nodes); do")
+          sw.IncIndent()
+          try:
+            sw.Write("if [[ -z \"$node1\" ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("tmp=\"$tmp $i $i:\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
+            sw.IncIndent()
+            try:
+              sw.Write("tmp=\"$tmp $i\"")
+            finally:
+              sw.DecIndent()
+            sw.Write("fi")
+          finally:
+            sw.DecIndent()
+          sw.Write("done")
+
+          WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
+        else:
+          WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
+      finally:
+        sw.DecIndent()
+
+      wrote_opt = True
+
+    if wrote_opt:
+      sw.Write("fi")
+
+    return
 
   def _CompleteArguments(self, sw):
     if not (self.opts or self.args):
@@ -288,10 +438,6 @@ class CompletionWriter:
       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):
@@ -313,24 +459,33 @@ class CompletionWriter:
           choices = "$(_ganeti_instances)"
         elif isinstance(arg, cli.ArgNode):
           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.ArgFile):
           choices = ""
           compgenargs.append("-f")
         elif isinstance(arg, cli.ArgCommand):
           choices = ""
           compgenargs.append("-c")
+        elif isinstance(arg, cli.ArgHost):
+          choices = ""
+          compgenargs.append("-A hostname")
         else:
           raise Exception("Unknown argument type %r" % arg)
 
         if arg.min == 1 and arg.max == 1:
           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
-        elif arg.min == arg.max:
-          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
-                     (last_arg_end, last_arg_end + arg.max))
         elif arg.max is None:
           cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
+        elif arg.min <= arg.max:
+          cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
+                     (last_arg_end, last_arg_end + arg.max))
         else:
           raise Exception("Unable to generate argument position condition")
 
@@ -348,7 +503,8 @@ class CompletionWriter:
             if choices:
               sw.Write("""choices="$choices "%s""", choices)
             if compgenargs:
-              sw.Write("compgenargs=%s", utils.ShellQuote(" ".join(compgenargs)))
+              sw.Write("compgenargs=%s",
+                       utils.ShellQuote(" ".join(compgenargs)))
           finally:
             sw.DecIndent()
 
@@ -369,7 +525,7 @@ class CompletionWriter:
     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.
@@ -387,14 +543,21 @@ def WriteCompletion(sw, scriptname, funcname,
   sw.Write("%s() {", funcname)
   sw.IncIndent()
   try:
-    sw.Write('local cur="$2" prev="$3"')
-    sw.Write("local i first_arg_idx choices compgenargs arg_idx")
+    sw.Write("local "
+             ' cur="${COMP_WORDS[COMP_CWORD]}"'
+             ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
+             ' i first_arg_idx choices compgenargs arg_idx optcur')
+
+    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
-      CompletionWriter(0, opts, args).WriteTo(sw)
+      CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
 
     else:
       sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
@@ -408,20 +571,25 @@ def WriteCompletion(sw, scriptname, funcname,
         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
+        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:
-          CompletionWriter(1, optdef, argdef).WriteTo(sw)
+          CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
         finally:
           sw.DecIndent()
-
         sw.Write(";;")
       sw.Write("esac")
   finally:
@@ -437,19 +605,6 @@ def GetFunctionName(name):
   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
 
 
-def LoadModule(filename):
-  """Loads an external module by filename.
-
-  """
-  (name, ext) = os.path.splitext(filename)
-
-  fh = open(filename, "U")
-  try:
-    return imp.load_module(name, fh, filename, (ext, "U", imp.PY_SOURCE))
-  finally:
-    fh.close()
-
-
 def GetCommands(filename, module):
   """Returns the commands defined in a module.
 
@@ -458,39 +613,209 @@ def GetCommands(filename, module):
   """
   try:
     commands = getattr(module, "commands")
-  except AttributeError, err:
+  except AttributeError:
     raise Exception("Script %s doesn't have 'commands' attribute" %
                     filename)
 
+  # Add the implicit "--help" option
+  help_option = cli.cli_option("-h", "--help", default=False,
+                               action="store_true")
+
+  for name, (_, _, optdef, _, _) in commands.items():
+    if help_option not in optdef:
+      optdef.append(help_option)
+    for opt in cli.COMMON_OPTS:
+      if opt in optdef:
+        raise Exception("Common option '%s' listed for command '%s' in %s" %
+                        (opt, name, filename))
+      optdef.append(opt)
+
   # Use aliases
   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
 
 
+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 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 = "./htools/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 = func_name.replace("-", "_")
+  output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
+  cli_opts = []
+  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
+      args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
+  WriteCompletion(sw, script_name, func_name, debug, opts=cli_opts, args=args)
+
+
 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")
+
   buf = StringIO()
-  sw = ShellWriter(buf)
+  sw = utils.ShellWriter(buf, indent=not options.compact)
+
+  # 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, not options.compact)
 
   # gnt-* scripts
   for scriptname in _autoconf.GNT_SCRIPTS:
     filename = "scripts/%s" % scriptname
 
-    WriteCompletion(sw, scriptname,
-                    GetFunctionName(scriptname),
-                    commands=GetCommands(filename, LoadModule(filename)))
+    WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
+                    not options.compact,
+                    commands=GetCommands(filename,
+                                         build.LoadModule(filename)))
 
   # Burnin script
-  burnin = LoadModule("tools/burnin")
-  WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
+  burnin = build.LoadModule("tools/burnin")
+  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
+                  not options.compact,
                   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=not options.compact)
+
+  # ganeti-confd, if enabled
+  if _autoconf.ENABLE_CONFD:
+    WriteHaskellCompletion(sw, "htools/ganeti-confd", htools=False,
+                           debug=not options.compact)
+
+  # Reset extglob to original value
+  sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
+  sw.Write("unset gnt_shopt_extglob")
+
   print buf.getvalue()