Merge remote branch 'devel-2.1'
[ganeti-local] / autotools / build-bash-completion
index 57729dd..79258c2 100755 (executable)
 # 02110-1301, USA.
 
 
 # 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 os
-import sys
 import re
 from cStringIO import StringIO
 
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
 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.
 
 # _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.
@@ -81,11 +86,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")
+    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("_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("}")
   finally:
     sw.DecIndent()
   sw.Write("}")
@@ -95,7 +122,7 @@ def WritePreamble(sw):
   try:
     instance_list_path = os.path.join(constants.DATA_DIR,
                                       "ssconf_instance_list")
   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("}")
   finally:
     sw.DecIndent()
   sw.Write("}")
@@ -104,22 +131,28 @@ 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=$( cd %s && echo job-*; )",
+    sw.Write(("local jlist=$( shopt -s nullglob &&"
+              " cd %s 2>/dev/null && echo job-* || : )"),
              utils.ShellQuote(constants.QUEUE_DIR))
              utils.ShellQuote(constants.QUEUE_DIR))
-    sw.Write("echo ${jlist//job-/}")
+    sw.Write('echo "${jlist//job-/}"')
   finally:
     sw.DecIndent()
   sw.Write("}")
 
   finally:
     sw.DecIndent()
   sw.Write("}")
 
-  sw.Write("_ganeti_os() {")
-  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))
-  finally:
-    sw.DecIndent()
-  sw.Write("}")
+  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("}")
 
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
 
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
@@ -180,9 +213,48 @@ def WritePreamble(sw):
     sw.DecIndent()
   sw.Write("}")
 
     sw.DecIndent()
   sw.Write("}")
 
+  # Params: <long options with equal sign> <all options>
+  # 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'\"")
+
+    sw.Write("return 1")
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
+  # Params: <compgen options>
+  # 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):
-  sw.Write("""COMPREPLY=( $(compgen %s -- "$cur") )""", args)
+def WriteCompReply(sw, args, cur="\"$cur\""):
+  sw.Write("_ganeti_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
   sw.Write("return")
 
 
@@ -196,6 +268,8 @@ class CompletionWriter:
     self.args = args
 
     for opt in opts:
     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):
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
   def _FindFirstArgument(self, sw):
@@ -206,7 +280,8 @@ class CompletionWriter:
       if opt.takes_value():
         # Ignore value
         for i in opt.all_names:
       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])
           skip_one.append(utils.ShellQuote(i))
       else:
         ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
@@ -237,37 +312,109 @@ class CompletionWriter:
       # Only static choices implemented so far (e.g. no node list)
       suggest = getattr(opt, "completion_suggest", None)
 
       # 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 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:
       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
 
 
     # 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_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):
 
   def _CompleteArguments(self, sw):
     if not (self.opts or self.args):
@@ -315,6 +462,8 @@ class CompletionWriter:
           choices = "$(_ganeti_nodes)"
         elif isinstance(arg, cli.ArgJobId):
           choices = "$(_ganeti_jobs)"
           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")
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
@@ -329,11 +478,11 @@ class CompletionWriter:
 
         if arg.min == 1 and arg.max == 1:
           cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
 
         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.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")
 
         else:
           raise Exception("Unable to generate argument position condition")
 
@@ -351,7 +500,8 @@ class CompletionWriter:
             if choices:
               sw.Write("""choices="$choices "%s""", choices)
             if compgenargs:
             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()
 
           finally:
             sw.DecIndent()
 
@@ -390,8 +540,14 @@ def WriteCompletion(sw, scriptname, funcname,
   sw.Write("%s() {", funcname)
   sw.IncIndent()
   try:
   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=()")
 
 
     sw.Write("COMPREPLY=()")
 
@@ -440,19 +596,6 @@ def GetFunctionName(name):
   return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
 
 
   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.
 
 def GetCommands(filename, module):
   """Returns the commands defined in a module.
 
@@ -461,10 +604,20 @@ def GetCommands(filename, module):
   """
   try:
     commands = getattr(module, "commands")
   """
   try:
     commands = getattr(module, "commands")
-  except AttributeError, err:
+  except AttributeError:
     raise Exception("Script %s doesn't have 'commands' attribute" %
                     filename)
 
     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 (_, _, optdef, _, _) in commands.itervalues():
+    if help_option not in optdef:
+      optdef.append(help_option)
+    if cli.DEBUG_OPT not in optdef:
+      optdef.append(cli.DEBUG_OPT)
+
   # Use aliases
   aliases = getattr(module, "aliases", {})
   if aliases:
   # Use aliases
   aliases = getattr(module, "aliases", {})
   if aliases:
@@ -487,10 +640,11 @@ def main():
 
     WriteCompletion(sw, scriptname,
                     GetFunctionName(scriptname),
 
     WriteCompletion(sw, scriptname,
                     GetFunctionName(scriptname),
-                    commands=GetCommands(filename, LoadModule(filename)))
+                    commands=GetCommands(filename,
+                                         build.LoadModule(filename)))
 
   # Burnin script
 
   # 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)
 
   WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
                   opts=burnin.OPTIONS, args=burnin.ARGUMENTS)