#!/usr/bin/python
#

# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""Script to generate bash_completion script for Ganeti.

"""

# pylint: disable=C0103
# [C0103] Invalid name build-bash-completion

import os
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

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

#: 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, support_debug):
  """Writes the script preamble.

  Helper functions should be written here.

  """
  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(pathutils.DATA_DIR, "ssconf_node_list")
    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
  finally:
    sw.DecIndent()
  sw.Write("}")

  sw.Write("_ganeti_instances() {")
  sw.IncIndent()
  try:
    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:
    sw.DecIndent()
  sw.Write("}")

  sw.Write("_ganeti_jobs() {")
  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(pathutils.QUEUE_DIR))
    sw.Write('echo "${jlist[@]/job-/}"')
  finally:
    sw.DecIndent()
  sw.Write("}")

  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:
    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("}")

  # Params: <offset> <options with values> <options without values>
  # Result variable: $first_arg_idx
  sw.Write("_ganeti_find_first_arg() {")
  sw.IncIndent()
  try:
    sw.Write("local w i")

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

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

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

      # Ah, we found the first argument
      sw.Write("else first_arg_idx=$i; break;")
      sw.Write("fi")
    finally:
      sw.DecIndent()
    sw.Write("done")
  finally:
    sw.DecIndent()
  sw.Write("}")

  # Params: <list of options separated by space>
  # Input variable: $first_arg_idx
  # Result variables: $arg_idx, $choices
  sw.Write("_ganeti_list_options() {")
  sw.IncIndent()
  try:
    sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
    sw.IncIndent()
    try:
      sw.Write("arg_idx=0")
      # Show options only if the current word starts with a dash
      sw.Write("""if [[ "$cur" == -* ]]; then""")
      sw.IncIndent()
      try:
        sw.Write("choices=$1")
      finally:
        sw.DecIndent()
      sw.Write("fi")
      sw.Write("return")
    finally:
      sw.DecIndent()
    sw.Write("fi")

    # Calculate position of current argument
    sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
    sw.Write("choices=")
  finally:
    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, cur="\"$cur\""):
  sw.Write("_gnt_compgen %s -- %s", args, cur)
  sw.Write("return")


class CompletionWriter:
  """Command completion writer class.

  """
  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 = []

    for opt in self.opts:
      if opt.takes_value():
        # Ignore value
        for i in opt.all_names:
          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])

    ignore = sorted(utils.UniqueSequence(ignore))
    skip_one = sorted(utils.UniqueSequence(skip_one))

    if ignore or skip_one:
      # Try to locate first argument
      sw.Write("_ganeti_find_first_arg %s %s %s",
               self.arg_offset + 1,
               utils.ShellQuote("|".join(skip_one)),
               utils.ShellQuote("|".join(ignore)))
    else:
      # When there are no options the first argument is always at position
      # offset + 1
      sw.Write("first_arg_idx=%s", self.arg_offset + 1)

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

    for opt in self.opts:
      if not opt.takes_value():
        continue

      # 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 (isinstance(suggest, (int, long)) and
          suggest in cli.OPT_COMPL_ALL):
        key = suggest
      elif suggest:
        key = " ".join(sorted(suggest))
      else:
        key = ""

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

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

    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_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_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):
      return

    all_option_names = []
    for opt in self.opts:
      all_option_names.extend(opt.all_names)
    all_option_names.sort()

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

    if self.args:
      last_idx = len(self.args) - 1
      last_arg_end = 0
      varlen_arg_idx = None
      wrote_arg = False

      sw.Write("compgenargs=")

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

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

        compgenargs = []

        if isinstance(arg, cli.ArgUnknown):
          choices = ""
        elif isinstance(arg, cli.ArgSuggest):
          choices = utils.ShellQuote(" ".join(arg.choices))
        elif isinstance(arg, cli.ArgInstance):
          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.ArgExtStorage):
          choices = "$(_ganeti_extstorage)"
        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.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")

        last_arg_end += arg.min

        if choices or compgenargs:
          if wrote_arg:
            condcmd = "elif"
          else:
            condcmd = "if"

          sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
          sw.IncIndent()
          try:
            if choices:
              sw.Write("""choices="$choices "%s""", choices)
            if compgenargs:
              sw.Write("compgenargs=%s",
                       utils.ShellQuote(" ".join(compgenargs)))
          finally:
            sw.DecIndent()

          wrote_arg = True

      if wrote_arg:
        sw.Write("fi")

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

  def WriteTo(self, sw):
    self._FindFirstArgument(sw)
    self._CompleteOptionValues(sw)
    self._CompleteArguments(sw)


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

  @type sw: ShellWriter
  @param sw: Script writer
  @type scriptname: string
  @param scriptname: Name of command line program
  @type funcname: string
  @param funcname: Shell function name
  @type commands: list
  @param commands: List of all subcommands in this program

  """
  sw.Write("%s() {", funcname)
  sw.IncIndent()
  try:
    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, support_debug).WriteTo(sw)

    else:
      sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
      sw.IncIndent()
      try:
        # Complete the command name
        WriteCompReply(sw,
                       ("-W %s" %
                        utils.ShellQuote(" ".join(sorted(commands.keys())))))
      finally:
        sw.DecIndent()
      sw.Write("fi")

      # 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)

      # 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, support_debug).WriteTo(sw)
        finally:
          sw.DecIndent()
        sw.Write(";;")
      sw.Write("esac")
  finally:
    sw.DecIndent()
  sw.Write("}")

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


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


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

  Aliases are also added as commands.

  """
  try:
    commands = getattr(module, "commands")
  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.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 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():
  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()
  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, debug)

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

    WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
                    commands=GetCommands(filename,
                                         build.LoadModule(filename)))

  # Burnin script
  WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
                  debug,
                  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()


if __name__ == "__main__":
  main()
