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
38 from ganeti import pathutils
40 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
41 # making an exception here because this script is only used at build time.
42 from ganeti import _autoconf
44 #: Regular expression describing desired format of option names. Long names can
45 #: contain lowercase characters, numbers and dashes only.
46 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
49 def WritePreamble(sw):
50 """Writes the script preamble.
52 Helper functions should be written here.
55 sw.Write("# This script is automatically generated at build time.")
56 sw.Write("# Do not modify manually.")
58 sw.Write("_gnt_log() {")
61 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
68 sw.Write("echo \"$@\"")
72 sw.Write("} >> $GANETI_COMPL_LOG")
80 sw.Write("_ganeti_nodes() {")
83 node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
84 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
89 sw.Write("_ganeti_instances() {")
92 instance_list_path = os.path.join(pathutils.DATA_DIR,
93 "ssconf_instance_list")
94 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
99 sw.Write("_ganeti_jobs() {")
102 # FIXME: this is really going into the internals of the job queue
103 sw.Write(("local jlist=$( shopt -s nullglob &&"
104 " cd %s 2>/dev/null && echo job-* || : )"),
105 utils.ShellQuote(pathutils.QUEUE_DIR))
106 sw.Write('echo "${jlist//job-/}"')
111 for (fnname, paths) in [
112 ("os", pathutils.OS_SEARCH_PATH),
113 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
115 sw.Write("_ganeti_%s() {", fnname)
118 # FIXME: Make querying the master for all OSes cheap
120 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
121 utils.ShellQuote(path))
126 sw.Write("_ganeti_nodegroup() {")
129 nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
130 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
135 # Params: <offset> <options with values> <options without values>
136 # Result variable: $first_arg_idx
137 sw.Write("_ganeti_find_first_arg() {")
140 sw.Write("local w i")
142 sw.Write("first_arg_idx=")
143 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
146 sw.Write("w=${COMP_WORDS[$i]}")
149 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
152 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
154 # Ah, we found the first argument
155 sw.Write("else first_arg_idx=$i; break;")
164 # Params: <list of options separated by space>
165 # Input variable: $first_arg_idx
166 # Result variables: $arg_idx, $choices
167 sw.Write("_ganeti_list_options() {")
170 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
173 sw.Write("arg_idx=0")
174 # Show options only if the current word starts with a dash
175 sw.Write("""if [[ "$cur" == -* ]]; then""")
178 sw.Write("choices=$1")
187 # Calculate position of current argument
188 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
194 # Params: <long options with equal sign> <all options>
195 # Result variable: $optcur
196 sw.Write("_gnt_checkopt() {")
199 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
202 sw.Write("optcur=\"${cur#--*=}\"")
206 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
209 sw.Write("optcur=\"$cur\"")
215 sw.Write("_gnt_log optcur=\"'$optcur'\"")
222 # Params: <compgen options>
223 # Result variable: $COMPREPLY
224 sw.Write("_gnt_compgen() {")
227 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
228 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
234 def WriteCompReply(sw, args, cur="\"$cur\""):
235 sw.Write("_gnt_compgen %s -- %s", args, cur)
239 class CompletionWriter:
240 """Command completion writer class.
243 def __init__(self, arg_offset, opts, args):
244 self.arg_offset = arg_offset
249 # While documented, these variables aren't seen as public attributes by
250 # pylint. pylint: disable=W0212
251 opt.all_names = sorted(opt._short_opts + opt._long_opts)
253 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
255 raise Exception("Option names don't match regular expression '%s': %s" %
256 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
258 def _FindFirstArgument(self, sw):
262 for opt in self.opts:
263 if opt.takes_value():
265 for i in opt.all_names:
266 if i.startswith("--"):
267 ignore.append("%s=*" % utils.ShellQuote(i))
268 skip_one.append(utils.ShellQuote(i))
270 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
272 ignore = sorted(utils.UniqueSequence(ignore))
273 skip_one = sorted(utils.UniqueSequence(skip_one))
275 if ignore or skip_one:
276 # Try to locate first argument
277 sw.Write("_ganeti_find_first_arg %s %s %s",
279 utils.ShellQuote("|".join(skip_one)),
280 utils.ShellQuote("|".join(ignore)))
282 # When there are no options the first argument is always at position
284 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
286 def _CompleteOptionValues(self, sw):
288 # "values" -> [optname1, optname2, ...]
291 for opt in self.opts:
292 if not opt.takes_value():
295 # Only static choices implemented so far (e.g. no node list)
296 suggest = getattr(opt, "completion_suggest", None)
298 # our custom option type
299 if opt.type == "bool":
300 suggest = ["yes", "no"]
303 suggest = opt.choices
305 if (isinstance(suggest, (int, long)) and
306 suggest in cli.OPT_COMPL_ALL):
309 key = " ".join(sorted(suggest))
313 values.setdefault(key, []).extend(opt.all_names)
315 # Don't write any code if there are no option values
323 for (suggest, allnames) in values.items():
324 longnames = [i for i in allnames if i.startswith("--")]
331 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
332 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
333 utils.ShellQuote("|".join(allnames)))
336 if suggest == cli.OPT_COMPL_MANY_NODES:
337 # TODO: Implement comma-separated values
338 WriteCompReply(sw, "-W ''", cur=cur)
339 elif suggest == cli.OPT_COMPL_ONE_NODE:
340 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
341 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
342 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
343 elif suggest == cli.OPT_COMPL_ONE_OS:
344 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
345 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
346 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
347 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
348 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
349 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
350 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
352 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
355 sw.Write("node1=\"${optcur%%:*}\"")
357 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
360 sw.Write("pfx=\"$node1:\"")
368 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
369 " node1=\"'$node1'\"")
371 sw.Write("for i in $(_ganeti_nodes); do")
374 sw.Write("if [[ -z \"$node1\" ]]; then")
377 sw.Write("tmp=\"$tmp $i $i:\"")
380 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
383 sw.Write("tmp=\"$tmp $i\"")
391 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
393 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
404 def _CompleteArguments(self, sw):
405 if not (self.opts or self.args):
408 all_option_names = []
409 for opt in self.opts:
410 all_option_names.extend(opt.all_names)
411 all_option_names.sort()
413 # List options if no argument has been specified yet
414 sw.Write("_ganeti_list_options %s",
415 utils.ShellQuote(" ".join(all_option_names)))
418 last_idx = len(self.args) - 1
420 varlen_arg_idx = None
423 sw.Write("compgenargs=")
425 for idx, arg in enumerate(self.args):
426 assert arg.min is not None and arg.min >= 0
427 assert not (idx < last_idx and arg.max is None)
429 if arg.min != arg.max or arg.max is None:
430 if varlen_arg_idx is not None:
431 raise Exception("Only one argument can have a variable length")
436 if isinstance(arg, cli.ArgUnknown):
438 elif isinstance(arg, cli.ArgSuggest):
439 choices = utils.ShellQuote(" ".join(arg.choices))
440 elif isinstance(arg, cli.ArgInstance):
441 choices = "$(_ganeti_instances)"
442 elif isinstance(arg, cli.ArgNode):
443 choices = "$(_ganeti_nodes)"
444 elif isinstance(arg, cli.ArgGroup):
445 choices = "$(_ganeti_nodegroup)"
446 elif isinstance(arg, cli.ArgJobId):
447 choices = "$(_ganeti_jobs)"
448 elif isinstance(arg, cli.ArgOs):
449 choices = "$(_ganeti_os)"
450 elif isinstance(arg, cli.ArgFile):
452 compgenargs.append("-f")
453 elif isinstance(arg, cli.ArgCommand):
455 compgenargs.append("-c")
456 elif isinstance(arg, cli.ArgHost):
458 compgenargs.append("-A hostname")
460 raise Exception("Unknown argument type %r" % arg)
462 if arg.min == 1 and arg.max == 1:
463 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
464 elif arg.max is None:
465 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
466 elif arg.min <= arg.max:
467 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
468 (last_arg_end, last_arg_end + arg.max))
470 raise Exception("Unable to generate argument position condition")
472 last_arg_end += arg.min
474 if choices or compgenargs:
480 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
484 sw.Write("""choices="$choices "%s""", choices)
486 sw.Write("compgenargs=%s",
487 utils.ShellQuote(" ".join(compgenargs)))
497 WriteCompReply(sw, """-W "$choices" $compgenargs""")
499 # $compgenargs exists only if there are arguments
500 WriteCompReply(sw, '-W "$choices"')
502 def WriteTo(self, sw):
503 self._FindFirstArgument(sw)
504 self._CompleteOptionValues(sw)
505 self._CompleteArguments(sw)
508 def WriteCompletion(sw, scriptname, funcname,
510 opts=None, args=None):
511 """Writes the completion code for one command.
513 @type sw: ShellWriter
514 @param sw: Script writer
515 @type scriptname: string
516 @param scriptname: Name of command line program
517 @type funcname: string
518 @param funcname: Shell function name
520 @param commands: List of all subcommands in this program
523 sw.Write("%s() {", funcname)
527 ' cur="${COMP_WORDS[COMP_CWORD]}"'
528 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
529 ' i first_arg_idx choices compgenargs arg_idx optcur')
531 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
532 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
533 " _gnt_log \"$(set | grep ^COMP_)\"")
535 sw.Write("COMPREPLY=()")
537 if opts is not None and args is not None:
539 CompletionWriter(0, opts, args).WriteTo(sw)
542 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
545 # Complete the command name
548 utils.ShellQuote(" ".join(sorted(commands.keys())))))
553 # Group commands by arguments and options
555 for cmd, (_, argdef, optdef, _, _) in commands.items():
556 if not (argdef or optdef):
558 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
560 # We're doing options and arguments to commands
561 sw.Write("""case "${COMP_WORDS[1]}" in""")
562 sort_grouped = sorted(grouped_cmds.items(),
563 key=lambda (_, y): sorted(y)[0])
564 for ((argdef, optdef), cmds) in sort_grouped:
565 assert argdef or optdef
566 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
569 CompletionWriter(1, optdef, argdef).WriteTo(sw)
578 sw.Write("complete -F %s -o filenames %s",
579 utils.ShellQuote(funcname),
580 utils.ShellQuote(scriptname))
583 def GetFunctionName(name):
584 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
587 def GetCommands(filename, module):
588 """Returns the commands defined in a module.
590 Aliases are also added as commands.
594 commands = getattr(module, "commands")
595 except AttributeError:
596 raise Exception("Script %s doesn't have 'commands' attribute" %
599 # Add the implicit "--help" option
600 help_option = cli.cli_option("-h", "--help", default=False,
603 for name, (_, _, optdef, _, _) in commands.items():
604 if help_option not in optdef:
605 optdef.append(help_option)
606 for opt in cli.COMMON_OPTS:
608 raise Exception("Common option '%s' listed for command '%s' in %s" %
609 (opt, name, filename))
613 aliases = getattr(module, "aliases", {})
615 commands = commands.copy()
616 for name, target in aliases.items():
617 commands[name] = commands[target]
624 sw = utils.ShellWriter(buf)
629 for scriptname in _autoconf.GNT_SCRIPTS:
630 filename = "scripts/%s" % scriptname
632 WriteCompletion(sw, scriptname,
633 GetFunctionName(scriptname),
634 commands=GetCommands(filename,
635 build.LoadModule(filename)))
638 burnin = build.LoadModule("tools/burnin")
639 WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
640 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
645 if __name__ == "__main__":