4 # Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Script to generate bash_completion script for Ganeti.
26 # pylint: disable=C0103
27 # [C0103] Invalid name build-bash-completion
33 from cStringIO import StringIO
35 from ganeti import constants
36 from ganeti import cli
37 from ganeti import utils
38 from ganeti import build
39 from ganeti import pathutils
41 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
42 # making an exception here because this script is only used at build time.
43 from ganeti import _autoconf
45 #: Regular expression describing desired format of option names. Long names can
46 #: contain lowercase characters, numbers and dashes only.
47 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
50 def WritePreamble(sw, support_debug):
51 """Writes the script preamble.
53 Helper functions should be written here.
56 sw.Write("# This script is automatically generated at build time.")
57 sw.Write("# Do not modify manually.")
60 sw.Write("_gnt_log() {")
63 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
70 sw.Write("echo \"$@\"")
74 sw.Write("} >> $GANETI_COMPL_LOG")
82 sw.Write("_ganeti_nodes() {")
85 node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
86 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
91 sw.Write("_ganeti_instances() {")
94 instance_list_path = os.path.join(pathutils.DATA_DIR,
95 "ssconf_instance_list")
96 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
101 sw.Write("_ganeti_jobs() {")
104 # FIXME: this is really going into the internals of the job queue
105 sw.Write(("local jlist=$( shopt -s nullglob &&"
106 " cd %s 2>/dev/null && echo job-* || : )"),
107 utils.ShellQuote(pathutils.QUEUE_DIR))
108 sw.Write('echo "${jlist//job-/}"')
113 for (fnname, paths) in [
114 ("os", pathutils.OS_SEARCH_PATH),
115 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
117 sw.Write("_ganeti_%s() {", fnname)
120 # FIXME: Make querying the master for all OSes cheap
122 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
123 utils.ShellQuote(path))
128 sw.Write("_ganeti_nodegroup() {")
131 nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
132 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
137 # Params: <offset> <options with values> <options without values>
138 # Result variable: $first_arg_idx
139 sw.Write("_ganeti_find_first_arg() {")
142 sw.Write("local w i")
144 sw.Write("first_arg_idx=")
145 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
148 sw.Write("w=${COMP_WORDS[$i]}")
151 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
154 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
156 # Ah, we found the first argument
157 sw.Write("else first_arg_idx=$i; break;")
166 # Params: <list of options separated by space>
167 # Input variable: $first_arg_idx
168 # Result variables: $arg_idx, $choices
169 sw.Write("_ganeti_list_options() {")
172 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
175 sw.Write("arg_idx=0")
176 # Show options only if the current word starts with a dash
177 sw.Write("""if [[ "$cur" == -* ]]; then""")
180 sw.Write("choices=$1")
189 # Calculate position of current argument
190 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
196 # Params: <long options with equal sign> <all options>
197 # Result variable: $optcur
198 sw.Write("_gnt_checkopt() {")
201 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
204 sw.Write("optcur=\"${cur#--*=}\"")
208 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
211 sw.Write("optcur=\"$cur\"")
218 sw.Write("_gnt_log optcur=\"'$optcur'\"")
225 # Params: <compgen options>
226 # Result variable: $COMPREPLY
227 sw.Write("_gnt_compgen() {")
230 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
232 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
238 def WriteCompReply(sw, args, cur="\"$cur\""):
239 sw.Write("_gnt_compgen %s -- %s", args, cur)
243 class CompletionWriter:
244 """Command completion writer class.
247 def __init__(self, arg_offset, opts, args, support_debug):
248 self.arg_offset = arg_offset
251 self.support_debug = support_debug
254 # While documented, these variables aren't seen as public attributes by
255 # pylint. pylint: disable=W0212
256 opt.all_names = sorted(opt._short_opts + opt._long_opts)
258 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
260 raise Exception("Option names don't match regular expression '%s': %s" %
261 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
263 def _FindFirstArgument(self, sw):
267 for opt in self.opts:
268 if opt.takes_value():
270 for i in opt.all_names:
271 if i.startswith("--"):
272 ignore.append("%s=*" % utils.ShellQuote(i))
273 skip_one.append(utils.ShellQuote(i))
275 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
277 ignore = sorted(utils.UniqueSequence(ignore))
278 skip_one = sorted(utils.UniqueSequence(skip_one))
280 if ignore or skip_one:
281 # Try to locate first argument
282 sw.Write("_ganeti_find_first_arg %s %s %s",
284 utils.ShellQuote("|".join(skip_one)),
285 utils.ShellQuote("|".join(ignore)))
287 # When there are no options the first argument is always at position
289 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
291 def _CompleteOptionValues(self, sw):
293 # "values" -> [optname1, optname2, ...]
296 for opt in self.opts:
297 if not opt.takes_value():
300 # Only static choices implemented so far (e.g. no node list)
301 suggest = getattr(opt, "completion_suggest", None)
303 # our custom option type
304 if opt.type == "bool":
305 suggest = ["yes", "no"]
308 suggest = opt.choices
310 if (isinstance(suggest, (int, long)) and
311 suggest in cli.OPT_COMPL_ALL):
314 key = " ".join(sorted(suggest))
318 values.setdefault(key, []).extend(opt.all_names)
320 # Don't write any code if there are no option values
328 for (suggest, allnames) in values.items():
329 longnames = [i for i in allnames if i.startswith("--")]
336 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
337 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
338 utils.ShellQuote("|".join(allnames)))
341 if suggest == cli.OPT_COMPL_MANY_NODES:
342 # TODO: Implement comma-separated values
343 WriteCompReply(sw, "-W ''", cur=cur)
344 elif suggest == cli.OPT_COMPL_ONE_NODE:
345 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
346 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
347 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
348 elif suggest == cli.OPT_COMPL_ONE_OS:
349 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
350 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
351 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
352 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
353 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
354 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
355 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
357 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
360 sw.Write("node1=\"${optcur%%:*}\"")
362 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
365 sw.Write("pfx=\"$node1:\"")
373 if self.support_debug:
374 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
375 " node1=\"'$node1'\"")
377 sw.Write("for i in $(_ganeti_nodes); do")
380 sw.Write("if [[ -z \"$node1\" ]]; then")
383 sw.Write("tmp=\"$tmp $i $i:\"")
386 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
389 sw.Write("tmp=\"$tmp $i\"")
397 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
399 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
410 def _CompleteArguments(self, sw):
411 if not (self.opts or self.args):
414 all_option_names = []
415 for opt in self.opts:
416 all_option_names.extend(opt.all_names)
417 all_option_names.sort()
419 # List options if no argument has been specified yet
420 sw.Write("_ganeti_list_options %s",
421 utils.ShellQuote(" ".join(all_option_names)))
424 last_idx = len(self.args) - 1
426 varlen_arg_idx = None
429 sw.Write("compgenargs=")
431 for idx, arg in enumerate(self.args):
432 assert arg.min is not None and arg.min >= 0
433 assert not (idx < last_idx and arg.max is None)
435 if arg.min != arg.max or arg.max is None:
436 if varlen_arg_idx is not None:
437 raise Exception("Only one argument can have a variable length")
442 if isinstance(arg, cli.ArgUnknown):
444 elif isinstance(arg, cli.ArgSuggest):
445 choices = utils.ShellQuote(" ".join(arg.choices))
446 elif isinstance(arg, cli.ArgInstance):
447 choices = "$(_ganeti_instances)"
448 elif isinstance(arg, cli.ArgNode):
449 choices = "$(_ganeti_nodes)"
450 elif isinstance(arg, cli.ArgGroup):
451 choices = "$(_ganeti_nodegroup)"
452 elif isinstance(arg, cli.ArgJobId):
453 choices = "$(_ganeti_jobs)"
454 elif isinstance(arg, cli.ArgOs):
455 choices = "$(_ganeti_os)"
456 elif isinstance(arg, cli.ArgFile):
458 compgenargs.append("-f")
459 elif isinstance(arg, cli.ArgCommand):
461 compgenargs.append("-c")
462 elif isinstance(arg, cli.ArgHost):
464 compgenargs.append("-A hostname")
466 raise Exception("Unknown argument type %r" % arg)
468 if arg.min == 1 and arg.max == 1:
469 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
470 elif arg.max is None:
471 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
472 elif arg.min <= arg.max:
473 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
474 (last_arg_end, last_arg_end + arg.max))
476 raise Exception("Unable to generate argument position condition")
478 last_arg_end += arg.min
480 if choices or compgenargs:
486 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
490 sw.Write("""choices="$choices "%s""", choices)
492 sw.Write("compgenargs=%s",
493 utils.ShellQuote(" ".join(compgenargs)))
503 WriteCompReply(sw, """-W "$choices" $compgenargs""")
505 # $compgenargs exists only if there are arguments
506 WriteCompReply(sw, '-W "$choices"')
508 def WriteTo(self, sw):
509 self._FindFirstArgument(sw)
510 self._CompleteOptionValues(sw)
511 self._CompleteArguments(sw)
514 def WriteCompletion(sw, scriptname, funcname, support_debug,
516 opts=None, args=None):
517 """Writes the completion code for one command.
519 @type sw: ShellWriter
520 @param sw: Script writer
521 @type scriptname: string
522 @param scriptname: Name of command line program
523 @type funcname: string
524 @param funcname: Shell function name
526 @param commands: List of all subcommands in this program
529 sw.Write("%s_inner() {", funcname)
532 sw.Write("local i first_arg_idx choices compgenargs arg_idx optcur"
533 ' cur="${COMP_WORDS[COMP_CWORD]}"'
534 ' prev="${COMP_WORDS[COMP_CWORD-1]}"')
537 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
538 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
539 " _gnt_log \"$(set | grep ^COMP_)\"")
541 sw.Write("COMPREPLY=()")
543 if opts is not None and args is not None:
545 CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
548 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
551 # Complete the command name
554 utils.ShellQuote(" ".join(sorted(commands.keys())))))
559 # Group commands by arguments and options
561 for cmd, (_, argdef, optdef, _, _) in commands.items():
562 if not (argdef or optdef):
564 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
566 # We're doing options and arguments to commands
567 sw.Write("""case "${COMP_WORDS[1]}" in""")
568 sort_grouped = sorted(grouped_cmds.items(),
569 key=lambda (_, y): sorted(y)[0])
570 for ((argdef, optdef), cmds) in sort_grouped:
571 assert argdef or optdef
572 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
575 CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
584 # Wrapper function to always enable extglob (needed for advanced pattern
586 sw.Write("%s() {", funcname)
589 # Get current state of extglob
590 sw.Write("local -r eg=$(shopt -p extglob || :)")
593 sw.Write("shopt -s extglob")
595 sw.Write("%s_inner \"$@\"", funcname)
597 # Reset extglob to original value
598 sw.Write("[[ -n \"$eg\" ]] && $eg")
603 sw.Write("complete -F %s -o filenames %s",
604 utils.ShellQuote(funcname),
605 utils.ShellQuote(scriptname))
608 def GetFunctionName(name):
609 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
612 def GetCommands(filename, module):
613 """Returns the commands defined in a module.
615 Aliases are also added as commands.
619 commands = getattr(module, "commands")
620 except AttributeError:
621 raise Exception("Script %s doesn't have 'commands' attribute" %
624 # Add the implicit "--help" option
625 help_option = cli.cli_option("-h", "--help", default=False,
628 for name, (_, _, optdef, _, _) in commands.items():
629 if help_option not in optdef:
630 optdef.append(help_option)
631 for opt in cli.COMMON_OPTS:
633 raise Exception("Common option '%s' listed for command '%s' in %s" %
634 (opt, name, filename))
638 aliases = getattr(module, "aliases", {})
640 commands = commands.copy()
641 for name, target in aliases.items():
642 commands[name] = commands[target]
648 parser = optparse.OptionParser(usage="%prog [--compact]")
649 parser.add_option("--compact", action="store_true",
650 help=("Don't indent output and don't include debugging"
653 options, args = parser.parse_args()
655 parser.error("Wrong number of arguments")
658 sw = utils.ShellWriter(buf, indent=not options.compact)
660 WritePreamble(sw, not options.compact)
663 for scriptname in _autoconf.GNT_SCRIPTS:
664 filename = "scripts/%s" % scriptname
666 WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
668 commands=GetCommands(filename,
669 build.LoadModule(filename)))
672 burnin = build.LoadModule("tools/burnin")
673 WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
675 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
680 if __name__ == "__main__":