X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/83ec79613683c36cf92a26e755dabded6a872080..0ae7d41310781fd7370ae4f0c8365da0e6d87e16:/autotools/build-bash-completion diff --git a/autotools/build-bash-completion b/autotools/build-bash-completion index 57729dd..b046543 100755 --- a/autotools/build-bash-completion +++ b/autotools/build-bash-completion @@ -19,59 +19,27 @@ # 02110-1301, USA. -import imp -import optparse +"""Script to generate bash_completion script for Ganeti. + +""" + +# pylint: disable-msg=C0103 +# [C0103] Invalid name build-bash-completion + import os -import sys import re from cStringIO import StringIO from ganeti import constants from ganeti import cli from ganeti import utils +from ganeti import build # _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 -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): """Writes the script preamble. @@ -81,11 +49,33 @@ def WritePreamble(sw): 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") + 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)) + sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path)) finally: sw.DecIndent() sw.Write("}") @@ -95,7 +85,7 @@ def WritePreamble(sw): try: instance_list_path = os.path.join(constants.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 +94,34 @@ 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-*; )", + 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('echo "${jlist//job-/}"') finally: sw.DecIndent() sw.Write("}") - sw.Write("_ganeti_os() {") + for (fnname, paths) in [ + ("os", constants.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(constants.DATA_DIR, "ssconf_nodegroups") + sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path)) finally: sw.DecIndent() sw.Write("}") @@ -180,9 +185,48 @@ def WritePreamble(sw): sw.DecIndent() sw.Write("}") + # Params: + # Result variable: $optcur + sw.Write("_ganeti_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") + + sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"") -def WriteCompReply(sw, args): - sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args) + sw.Write("return 1") + finally: + sw.DecIndent() + sw.Write("}") + + # Params: + # Result variable: $COMPREPLY + sw.Write("_ganeti_compgen() {") + sw.IncIndent() + try: + sw.Write("""COMPREPLY=( $(compgen "$@") )""") + sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"") + finally: + sw.DecIndent() + sw.Write("}") + + +def WriteCompReply(sw, args, cur="\"$cur\""): + sw.Write("_ganeti_compgen %s -- %s", args, cur) sw.Write("return") @@ -196,6 +240,8 @@ class CompletionWriter: self.args = args for opt in opts: + # While documented, these variables aren't seen as public attributes by + # pylint. pylint: disable-msg=W0212 opt.all_names = sorted(opt._short_opts + opt._long_opts) def _FindFirstArgument(self, sw): @@ -206,7 +252,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 +284,111 @@ 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.iteritems(): + longnames = [i for i in allnames if i.startswith("--")] + + if wrote_opt: + condcmd = "elif" + else: + condcmd = "if" + + sw.Write("%s _ganeti_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_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") + + sw.Write("_ganeti_dbglog 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): @@ -315,6 +436,8 @@ class CompletionWriter: choices = "$(_ganeti_nodes)" 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") @@ -329,11 +452,11 @@ class CompletionWriter: 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") @@ -351,7 +474,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() @@ -390,8 +514,14 @@ 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') + + sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"") + sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&" + " _ganeti_dbglog \"$(set | grep ^COMP_)\"") sw.Write("COMPREPLY=()") @@ -440,19 +570,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. @@ -461,10 +578,23 @@ 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: @@ -477,7 +607,7 @@ def GetCommands(filename, module): def main(): buf = StringIO() - sw = ShellWriter(buf) + sw = utils.ShellWriter(buf) WritePreamble(sw) @@ -487,10 +617,11 @@ def main(): WriteCompletion(sw, scriptname, GetFunctionName(scriptname), - commands=GetCommands(filename, LoadModule(filename))) + commands=GetCommands(filename, + build.LoadModule(filename))) # Burnin script - burnin = LoadModule("tools/burnin") + burnin = build.LoadModule("tools/burnin") WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin", opts=burnin.OPTIONS, args=burnin.ARGUMENTS)