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
32 from cStringIO import StringIO
34 from ganeti import constants
35 from ganeti import cli
36 from ganeti import utils
37 from ganeti import build
39 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
40 # making an exception here because this script is only used at build time.
41 from ganeti import _autoconf
43 #: Regular expression describing desired format of option names. Long names can
44 #: contain lowercase characters, numbers and dashes only.
45 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
48 def WritePreamble(sw):
49 """Writes the script preamble.
51 Helper functions should be written here.
54 sw.Write("# This script is automatically generated at build time.")
55 sw.Write("# Do not modify manually.")
57 sw.Write("_gnt_log() {")
60 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
67 sw.Write("echo \"$@\"")
71 sw.Write("} >> $GANETI_COMPL_LOG")
79 sw.Write("_ganeti_nodes() {")
82 node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
83 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
88 sw.Write("_ganeti_instances() {")
91 instance_list_path = os.path.join(constants.DATA_DIR,
92 "ssconf_instance_list")
93 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
98 sw.Write("_ganeti_jobs() {")
101 # FIXME: this is really going into the internals of the job queue
102 sw.Write(("local jlist=$( shopt -s nullglob &&"
103 " cd %s 2>/dev/null && echo job-* || : )"),
104 utils.ShellQuote(constants.QUEUE_DIR))
105 sw.Write('echo "${jlist//job-/}"')
110 for (fnname, paths) in [
111 ("os", constants.OS_SEARCH_PATH),
112 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
114 sw.Write("_ganeti_%s() {", fnname)
117 # FIXME: Make querying the master for all OSes cheap
119 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
120 utils.ShellQuote(path))
125 sw.Write("_ganeti_nodegroup() {")
128 nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
129 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
134 sw.Write("_ganeti_network() {")
137 networks_path = os.path.join(constants.DATA_DIR, "ssconf_networks")
138 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
143 # Params: <offset> <options with values> <options without values>
144 # Result variable: $first_arg_idx
145 sw.Write("_ganeti_find_first_arg() {")
148 sw.Write("local w i")
150 sw.Write("first_arg_idx=")
151 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
154 sw.Write("w=${COMP_WORDS[$i]}")
157 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
160 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
162 # Ah, we found the first argument
163 sw.Write("else first_arg_idx=$i; break;")
172 # Params: <list of options separated by space>
173 # Input variable: $first_arg_idx
174 # Result variables: $arg_idx, $choices
175 sw.Write("_ganeti_list_options() {")
178 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
181 sw.Write("arg_idx=0")
182 # Show options only if the current word starts with a dash
183 sw.Write("""if [[ "$cur" == -* ]]; then""")
186 sw.Write("choices=$1")
195 # Calculate position of current argument
196 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
202 # Params: <long options with equal sign> <all options>
203 # Result variable: $optcur
204 sw.Write("_gnt_checkopt() {")
207 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
210 sw.Write("optcur=\"${cur#--*=}\"")
214 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
217 sw.Write("optcur=\"$cur\"")
223 sw.Write("_gnt_log optcur=\"'$optcur'\"")
230 # Params: <compgen options>
231 # Result variable: $COMPREPLY
232 sw.Write("_gnt_compgen() {")
235 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
236 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
242 def WriteCompReply(sw, args, cur="\"$cur\""):
243 sw.Write("_gnt_compgen %s -- %s", args, cur)
247 class CompletionWriter:
248 """Command completion writer class.
251 def __init__(self, arg_offset, opts, args):
252 self.arg_offset = arg_offset
257 # While documented, these variables aren't seen as public attributes by
258 # pylint. pylint: disable=W0212
259 opt.all_names = sorted(opt._short_opts + opt._long_opts)
261 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
263 raise Exception("Option names don't match regular expression '%s': %s" %
264 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
266 def _FindFirstArgument(self, sw):
270 for opt in self.opts:
271 if opt.takes_value():
273 for i in opt.all_names:
274 if i.startswith("--"):
275 ignore.append("%s=*" % utils.ShellQuote(i))
276 skip_one.append(utils.ShellQuote(i))
278 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
280 ignore = sorted(utils.UniqueSequence(ignore))
281 skip_one = sorted(utils.UniqueSequence(skip_one))
283 if ignore or skip_one:
284 # Try to locate first argument
285 sw.Write("_ganeti_find_first_arg %s %s %s",
287 utils.ShellQuote("|".join(skip_one)),
288 utils.ShellQuote("|".join(ignore)))
290 # When there are no options the first argument is always at position
292 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
294 def _CompleteOptionValues(self, sw):
296 # "values" -> [optname1, optname2, ...]
299 for opt in self.opts:
300 if not opt.takes_value():
303 # Only static choices implemented so far (e.g. no node list)
304 suggest = getattr(opt, "completion_suggest", None)
306 # our custom option type
307 if opt.type == "bool":
308 suggest = ["yes", "no"]
311 suggest = opt.choices
313 if (isinstance(suggest, (int, long)) and
314 suggest in cli.OPT_COMPL_ALL):
317 key = " ".join(sorted(suggest))
321 values.setdefault(key, []).extend(opt.all_names)
323 # Don't write any code if there are no option values
331 for (suggest, allnames) in values.items():
332 longnames = [i for i in allnames if i.startswith("--")]
339 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
340 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
341 utils.ShellQuote("|".join(allnames)))
344 if suggest == cli.OPT_COMPL_MANY_NODES:
345 # TODO: Implement comma-separated values
346 WriteCompReply(sw, "-W ''", cur=cur)
347 elif suggest == cli.OPT_COMPL_ONE_NODE:
348 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
349 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
350 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
351 elif suggest == cli.OPT_COMPL_ONE_OS:
352 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
353 elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
354 WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
355 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
356 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
357 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
358 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
359 elif suggest == cli.OPT_COMPL_ONE_NETWORK:
360 WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
361 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
362 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
364 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
367 sw.Write("node1=\"${optcur%%:*}\"")
369 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
372 sw.Write("pfx=\"$node1:\"")
380 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
381 " node1=\"'$node1'\"")
383 sw.Write("for i in $(_ganeti_nodes); do")
386 sw.Write("if [[ -z \"$node1\" ]]; then")
389 sw.Write("tmp=\"$tmp $i $i:\"")
392 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
395 sw.Write("tmp=\"$tmp $i\"")
403 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
405 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
416 def _CompleteArguments(self, sw):
417 if not (self.opts or self.args):
420 all_option_names = []
421 for opt in self.opts:
422 all_option_names.extend(opt.all_names)
423 all_option_names.sort()
425 # List options if no argument has been specified yet
426 sw.Write("_ganeti_list_options %s",
427 utils.ShellQuote(" ".join(all_option_names)))
430 last_idx = len(self.args) - 1
432 varlen_arg_idx = None
435 sw.Write("compgenargs=")
437 for idx, arg in enumerate(self.args):
438 assert arg.min is not None and arg.min >= 0
439 assert not (idx < last_idx and arg.max is None)
441 if arg.min != arg.max or arg.max is None:
442 if varlen_arg_idx is not None:
443 raise Exception("Only one argument can have a variable length")
448 if isinstance(arg, cli.ArgUnknown):
450 elif isinstance(arg, cli.ArgSuggest):
451 choices = utils.ShellQuote(" ".join(arg.choices))
452 elif isinstance(arg, cli.ArgInstance):
453 choices = "$(_ganeti_instances)"
454 elif isinstance(arg, cli.ArgNode):
455 choices = "$(_ganeti_nodes)"
456 elif isinstance(arg, cli.ArgGroup):
457 choices = "$(_ganeti_nodegroup)"
458 elif isinstance(arg, cli.ArgNetwork):
459 choices = "$(_ganeti_network)"
460 elif isinstance(arg, cli.ArgJobId):
461 choices = "$(_ganeti_jobs)"
462 elif isinstance(arg, cli.ArgOs):
463 choices = "$(_ganeti_os)"
464 elif isinstance(arg, cli.ArgExtStorage):
465 choices = "$(_ganeti_extstorage)"
466 elif isinstance(arg, cli.ArgFile):
468 compgenargs.append("-f")
469 elif isinstance(arg, cli.ArgCommand):
471 compgenargs.append("-c")
472 elif isinstance(arg, cli.ArgHost):
474 compgenargs.append("-A hostname")
476 raise Exception("Unknown argument type %r" % arg)
478 if arg.min == 1 and arg.max == 1:
479 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
480 elif arg.max is None:
481 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
482 elif arg.min <= arg.max:
483 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
484 (last_arg_end, last_arg_end + arg.max))
486 raise Exception("Unable to generate argument position condition")
488 last_arg_end += arg.min
490 if choices or compgenargs:
496 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
500 sw.Write("""choices="$choices "%s""", choices)
502 sw.Write("compgenargs=%s",
503 utils.ShellQuote(" ".join(compgenargs)))
513 WriteCompReply(sw, """-W "$choices" $compgenargs""")
515 # $compgenargs exists only if there are arguments
516 WriteCompReply(sw, '-W "$choices"')
518 def WriteTo(self, sw):
519 self._FindFirstArgument(sw)
520 self._CompleteOptionValues(sw)
521 self._CompleteArguments(sw)
524 def WriteCompletion(sw, scriptname, funcname,
526 opts=None, args=None):
527 """Writes the completion code for one command.
529 @type sw: ShellWriter
530 @param sw: Script writer
531 @type scriptname: string
532 @param scriptname: Name of command line program
533 @type funcname: string
534 @param funcname: Shell function name
536 @param commands: List of all subcommands in this program
539 sw.Write("%s() {", funcname)
543 ' cur="${COMP_WORDS[COMP_CWORD]}"'
544 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
545 ' i first_arg_idx choices compgenargs arg_idx optcur')
547 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
548 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
549 " _gnt_log \"$(set | grep ^COMP_)\"")
551 sw.Write("COMPREPLY=()")
553 if opts is not None and args is not None:
555 CompletionWriter(0, opts, args).WriteTo(sw)
558 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
561 # Complete the command name
564 utils.ShellQuote(" ".join(sorted(commands.keys())))))
569 # Group commands by arguments and options
571 for cmd, (_, argdef, optdef, _, _) in commands.items():
572 if not (argdef or optdef):
574 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
576 # We're doing options and arguments to commands
577 sw.Write("""case "${COMP_WORDS[1]}" in""")
578 sort_grouped = sorted(grouped_cmds.items(),
579 key=lambda (_, y): sorted(y)[0])
580 for ((argdef, optdef), cmds) in sort_grouped:
581 assert argdef or optdef
582 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
585 CompletionWriter(1, optdef, argdef).WriteTo(sw)
594 sw.Write("complete -F %s -o filenames %s",
595 utils.ShellQuote(funcname),
596 utils.ShellQuote(scriptname))
599 def GetFunctionName(name):
600 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
603 def GetCommands(filename, module):
604 """Returns the commands defined in a module.
606 Aliases are also added as commands.
610 commands = getattr(module, "commands")
611 except AttributeError:
612 raise Exception("Script %s doesn't have 'commands' attribute" %
615 # Add the implicit "--help" option
616 help_option = cli.cli_option("-h", "--help", default=False,
619 for name, (_, _, optdef, _, _) in commands.items():
620 if help_option not in optdef:
621 optdef.append(help_option)
622 for opt in cli.COMMON_OPTS:
624 raise Exception("Common option '%s' listed for command '%s' in %s" %
625 (opt, name, filename))
629 aliases = getattr(module, "aliases", {})
631 commands = commands.copy()
632 for name, target in aliases.items():
633 commands[name] = commands[target]
640 sw = utils.ShellWriter(buf)
642 # Remember original state of extglob and enable it (required for pattern
643 # matching; must be enabled while parsing script)
644 sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
645 sw.Write("shopt -s extglob")
650 for scriptname in _autoconf.GNT_SCRIPTS:
651 filename = "scripts/%s" % scriptname
653 WriteCompletion(sw, scriptname,
654 GetFunctionName(scriptname),
655 commands=GetCommands(filename,
656 build.LoadModule(filename)))
659 burnin = build.LoadModule("tools/burnin")
660 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
661 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
663 # Reset extglob to original value
664 sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
665 sw.Write("unset gnt_shopt_extglob")
670 if __name__ == "__main__":