X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/63d44c55891801fea72be9f872c67fad8b38481c..8bc78021d42eb97586015fa9b06932540fd8ada3:/autotools/build-bash-completion diff --git a/autotools/build-bash-completion b/autotools/build-bash-completion index 6f39ef5..63def12 100755 --- a/autotools/build-bash-completion +++ b/autotools/build-bash-completion @@ -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 @@ -19,60 +19,38 @@ # 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 + +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]+$") -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,10 +59,33 @@ 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") + 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() @@ -93,7 +94,7 @@ 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 2>/dev/null || :", utils.ShellQuote(instance_list_path)) finally: @@ -104,18 +105,18 @@ def WritePreamble(sw): 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 [ - ("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: @@ -127,6 +128,24 @@ def WritePreamble(sw): 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: # Result variable: $first_arg_idx sw.Write("_ganeti_find_first_arg() {") @@ -188,7 +207,7 @@ def WritePreamble(sw): # Params: # Result variable: $optcur - sw.Write("_ganeti_checkopt() {") + sw.Write("_gnt_checkopt() {") sw.IncIndent() try: sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""") @@ -207,14 +226,29 @@ def WritePreamble(sw): sw.DecIndent() sw.Write("fi") + if support_debug: + sw.Write("_gnt_log optcur=\"'$optcur'\"") + sw.Write("return 1") finally: sw.DecIndent() sw.Write("}") + # Params: + # 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("""COMPREPLY=( $(compgen %s -- %s) )""", args, cur) + sw.Write("_gnt_compgen %s -- %s", args, cur) sw.Write("return") @@ -222,14 +256,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 = [] @@ -270,6 +312,10 @@ 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 @@ -291,7 +337,7 @@ class CompletionWriter: 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: @@ -299,7 +345,7 @@ class CompletionWriter: 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() @@ -313,8 +359,58 @@ class CompletionWriter: 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: @@ -346,10 +442,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): @@ -371,8 +463,16 @@ 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.ArgExtStorage): + choices = "$(_ganeti_extstorage)" elif isinstance(arg, cli.ArgFile): choices = "" compgenargs.append("-f") @@ -387,11 +487,11 @@ class CompletionWriter: 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)) - elif arg.max is None: - cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end) else: raise Exception("Unable to generate argument position condition") @@ -409,7 +509,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() @@ -430,7 +531,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. @@ -453,15 +554,16 @@ def WriteCompletion(sw, scriptname, funcname, ' prev="${COMP_WORDS[COMP_CWORD-1]}"' ' i first_arg_idx choices compgenargs arg_idx optcur') - # Useful for debugging: - #sw.Write("echo cur=\"$cur\" prev=\"$prev\"") - #sw.Write("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 - CompletionWriter(0, opts, args).WriteTo(sw) + CompletionWriter(0, opts, args, support_debug).WriteTo(sw) else: sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""") @@ -475,20 +577,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: @@ -504,19 +611,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. @@ -525,39 +619,253 @@ 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 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 = 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 - WriteCompletion(sw, scriptname, - GetFunctionName(scriptname), - commands=GetCommands(filename, LoadModule(filename))) + WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug, + commands=GetCommands(filename, + build.LoadModule(filename))) # Burnin script - burnin = 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) + # 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()