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
34 from cStringIO import StringIO
36 from ganeti import constants
37 from ganeti import cli
38 from ganeti import utils
39 from ganeti import build
40 from ganeti import pathutils
42 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
43 # making an exception here because this script is only used at build time.
44 from ganeti import _autoconf
46 #: Regular expression describing desired format of option names. Long names can
47 #: contain lowercase characters, numbers and dashes only.
48 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
51 def WritePreamble(sw, support_debug):
52 """Writes the script preamble.
54 Helper functions should be written here.
57 sw.Write("# This script is automatically generated at build time.")
58 sw.Write("# Do not modify manually.")
61 sw.Write("_gnt_log() {")
64 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
71 sw.Write("echo \"$@\"")
75 sw.Write("} >> $GANETI_COMPL_LOG")
83 sw.Write("_ganeti_nodes() {")
86 node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
87 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
92 sw.Write("_ganeti_instances() {")
95 instance_list_path = os.path.join(pathutils.DATA_DIR,
96 "ssconf_instance_list")
97 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
102 sw.Write("_ganeti_jobs() {")
105 # FIXME: this is really going into the internals of the job queue
106 sw.Write(("local jlist=$( shopt -s nullglob &&"
107 " cd %s 2>/dev/null && echo job-* || : )"),
108 utils.ShellQuote(pathutils.QUEUE_DIR))
109 sw.Write('echo "${jlist//job-/}"')
114 for (fnname, paths) in [
115 ("os", pathutils.OS_SEARCH_PATH),
116 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
118 sw.Write("_ganeti_%s() {", fnname)
121 # FIXME: Make querying the master for all OSes cheap
123 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
124 utils.ShellQuote(path))
129 sw.Write("_ganeti_nodegroup() {")
132 nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
133 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
138 # Params: <offset> <options with values> <options without values>
139 # Result variable: $first_arg_idx
140 sw.Write("_ganeti_find_first_arg() {")
143 sw.Write("local w i")
145 sw.Write("first_arg_idx=")
146 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
149 sw.Write("w=${COMP_WORDS[$i]}")
152 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
155 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
157 # Ah, we found the first argument
158 sw.Write("else first_arg_idx=$i; break;")
167 # Params: <list of options separated by space>
168 # Input variable: $first_arg_idx
169 # Result variables: $arg_idx, $choices
170 sw.Write("_ganeti_list_options() {")
173 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
176 sw.Write("arg_idx=0")
177 # Show options only if the current word starts with a dash
178 sw.Write("""if [[ "$cur" == -* ]]; then""")
181 sw.Write("choices=$1")
190 # Calculate position of current argument
191 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
197 # Params: <long options with equal sign> <all options>
198 # Result variable: $optcur
199 sw.Write("_gnt_checkopt() {")
202 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
205 sw.Write("optcur=\"${cur#--*=}\"")
209 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
212 sw.Write("optcur=\"$cur\"")
219 sw.Write("_gnt_log optcur=\"'$optcur'\"")
226 # Params: <compgen options>
227 # Result variable: $COMPREPLY
228 sw.Write("_gnt_compgen() {")
231 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
233 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
239 def WriteCompReply(sw, args, cur="\"$cur\""):
240 sw.Write("_gnt_compgen %s -- %s", args, cur)
244 class CompletionWriter:
245 """Command completion writer class.
248 def __init__(self, arg_offset, opts, args, support_debug):
249 self.arg_offset = arg_offset
252 self.support_debug = support_debug
255 # While documented, these variables aren't seen as public attributes by
256 # pylint. pylint: disable=W0212
257 opt.all_names = sorted(opt._short_opts + opt._long_opts)
259 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
261 raise Exception("Option names don't match regular expression '%s': %s" %
262 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
264 def _FindFirstArgument(self, sw):
268 for opt in self.opts:
269 if opt.takes_value():
271 for i in opt.all_names:
272 if i.startswith("--"):
273 ignore.append("%s=*" % utils.ShellQuote(i))
274 skip_one.append(utils.ShellQuote(i))
276 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
278 ignore = sorted(utils.UniqueSequence(ignore))
279 skip_one = sorted(utils.UniqueSequence(skip_one))
281 if ignore or skip_one:
282 # Try to locate first argument
283 sw.Write("_ganeti_find_first_arg %s %s %s",
285 utils.ShellQuote("|".join(skip_one)),
286 utils.ShellQuote("|".join(ignore)))
288 # When there are no options the first argument is always at position
290 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
292 def _CompleteOptionValues(self, sw):
294 # "values" -> [optname1, optname2, ...]
297 for opt in self.opts:
298 if not opt.takes_value():
301 # Only static choices implemented so far (e.g. no node list)
302 suggest = getattr(opt, "completion_suggest", None)
304 # our custom option type
305 if opt.type == "bool":
306 suggest = ["yes", "no"]
309 suggest = opt.choices
311 if (isinstance(suggest, (int, long)) and
312 suggest in cli.OPT_COMPL_ALL):
315 key = " ".join(sorted(suggest))
319 values.setdefault(key, []).extend(opt.all_names)
321 # Don't write any code if there are no option values
329 for (suggest, allnames) in values.items():
330 longnames = [i for i in allnames if i.startswith("--")]
337 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
338 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
339 utils.ShellQuote("|".join(allnames)))
342 if suggest == cli.OPT_COMPL_MANY_NODES:
343 # TODO: Implement comma-separated values
344 WriteCompReply(sw, "-W ''", cur=cur)
345 elif suggest == cli.OPT_COMPL_ONE_NODE:
346 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
347 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
348 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
349 elif suggest == cli.OPT_COMPL_ONE_OS:
350 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
351 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
352 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
353 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
354 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
355 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
356 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
358 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
361 sw.Write("node1=\"${optcur%%:*}\"")
363 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
366 sw.Write("pfx=\"$node1:\"")
374 if self.support_debug:
375 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
376 " node1=\"'$node1'\"")
378 sw.Write("for i in $(_ganeti_nodes); do")
381 sw.Write("if [[ -z \"$node1\" ]]; then")
384 sw.Write("tmp=\"$tmp $i $i:\"")
387 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
390 sw.Write("tmp=\"$tmp $i\"")
398 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
400 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
411 def _CompleteArguments(self, sw):
412 if not (self.opts or self.args):
415 all_option_names = []
416 for opt in self.opts:
417 all_option_names.extend(opt.all_names)
418 all_option_names.sort()
420 # List options if no argument has been specified yet
421 sw.Write("_ganeti_list_options %s",
422 utils.ShellQuote(" ".join(all_option_names)))
425 last_idx = len(self.args) - 1
427 varlen_arg_idx = None
430 sw.Write("compgenargs=")
432 for idx, arg in enumerate(self.args):
433 assert arg.min is not None and arg.min >= 0
434 assert not (idx < last_idx and arg.max is None)
436 if arg.min != arg.max or arg.max is None:
437 if varlen_arg_idx is not None:
438 raise Exception("Only one argument can have a variable length")
443 if isinstance(arg, cli.ArgUnknown):
445 elif isinstance(arg, cli.ArgSuggest):
446 choices = utils.ShellQuote(" ".join(arg.choices))
447 elif isinstance(arg, cli.ArgInstance):
448 choices = "$(_ganeti_instances)"
449 elif isinstance(arg, cli.ArgNode):
450 choices = "$(_ganeti_nodes)"
451 elif isinstance(arg, cli.ArgGroup):
452 choices = "$(_ganeti_nodegroup)"
453 elif isinstance(arg, cli.ArgJobId):
454 choices = "$(_ganeti_jobs)"
455 elif isinstance(arg, cli.ArgOs):
456 choices = "$(_ganeti_os)"
457 elif isinstance(arg, cli.ArgFile):
459 compgenargs.append("-f")
460 elif isinstance(arg, cli.ArgCommand):
462 compgenargs.append("-c")
463 elif isinstance(arg, cli.ArgHost):
465 compgenargs.append("-A hostname")
467 raise Exception("Unknown argument type %r" % arg)
469 if arg.min == 1 and arg.max == 1:
470 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
471 elif arg.max is None:
472 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
473 elif arg.min <= arg.max:
474 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
475 (last_arg_end, last_arg_end + arg.max))
477 raise Exception("Unable to generate argument position condition")
479 last_arg_end += arg.min
481 if choices or compgenargs:
487 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
491 sw.Write("""choices="$choices "%s""", choices)
493 sw.Write("compgenargs=%s",
494 utils.ShellQuote(" ".join(compgenargs)))
504 WriteCompReply(sw, """-W "$choices" $compgenargs""")
506 # $compgenargs exists only if there are arguments
507 WriteCompReply(sw, '-W "$choices"')
509 def WriteTo(self, sw):
510 self._FindFirstArgument(sw)
511 self._CompleteOptionValues(sw)
512 self._CompleteArguments(sw)
515 def WriteCompletion(sw, scriptname, funcname, support_debug,
517 opts=None, args=None):
518 """Writes the completion code for one command.
520 @type sw: ShellWriter
521 @param sw: Script writer
522 @type scriptname: string
523 @param scriptname: Name of command line program
524 @type funcname: string
525 @param funcname: Shell function name
527 @param commands: List of all subcommands in this program
530 sw.Write("%s() {", funcname)
534 ' cur="${COMP_WORDS[COMP_CWORD]}"'
535 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
536 ' i first_arg_idx choices compgenargs arg_idx optcur')
539 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
540 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
541 " _gnt_log \"$(set | grep ^COMP_)\"")
543 sw.Write("COMPREPLY=()")
545 if opts is not None and args is not None:
547 CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
550 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
553 # Complete the command name
556 utils.ShellQuote(" ".join(sorted(commands.keys())))))
561 # Group commands by arguments and options
563 for cmd, (_, argdef, optdef, _, _) in commands.items():
564 if not (argdef or optdef):
566 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
568 # We're doing options and arguments to commands
569 sw.Write("""case "${COMP_WORDS[1]}" in""")
570 sort_grouped = sorted(grouped_cmds.items(),
571 key=lambda (_, y): sorted(y)[0])
572 for ((argdef, optdef), cmds) in sort_grouped:
573 assert argdef or optdef
574 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
577 CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
586 sw.Write("complete -F %s -o filenames %s",
587 utils.ShellQuote(funcname),
588 utils.ShellQuote(scriptname))
591 def GetFunctionName(name):
592 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
595 def GetCommands(filename, module):
596 """Returns the commands defined in a module.
598 Aliases are also added as commands.
602 commands = getattr(module, "commands")
603 except AttributeError:
604 raise Exception("Script %s doesn't have 'commands' attribute" %
607 # Add the implicit "--help" option
608 help_option = cli.cli_option("-h", "--help", default=False,
611 for name, (_, _, optdef, _, _) in commands.items():
612 if help_option not in optdef:
613 optdef.append(help_option)
614 for opt in cli.COMMON_OPTS:
616 raise Exception("Common option '%s' listed for command '%s' in %s" %
617 (opt, name, filename))
621 aliases = getattr(module, "aliases", {})
623 commands = commands.copy()
624 for name, target in aliases.items():
625 commands[name] = commands[target]
630 def HaskellOptToOptParse(opts, kind):
631 """Converts a Haskell options to Python cli_options.
634 @param opts: comma-separated string with short and long options
636 @param kind: type generated by Common.hs/complToText; needs to be
640 # pylint: disable=W0142
641 # since we pass *opts in a number of places
642 opts = opts.split(",")
644 return cli.cli_option(*opts, action="store_true")
645 elif kind in ["file", "string", "host", "dir", "inetaddr"]:
646 return cli.cli_option(*opts, type="string")
647 elif kind == "integer":
648 return cli.cli_option(*opts, type="int")
649 elif kind == "float":
650 return cli.cli_option(*opts, type="float")
651 elif kind == "onegroup":
652 return cli.cli_option(*opts, type="string",
653 completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
654 elif kind == "onenode":
655 return cli.cli_option(*opts, type="string",
656 completion_suggest=cli.OPT_COMPL_ONE_NODE)
657 elif kind == "manyinstances":
658 # FIXME: no support for many instances
659 return cli.cli_option(*opts, type="string")
660 elif kind.startswith("choices="):
661 choices = kind[len("choices="):].split(",")
662 return cli.cli_option(*opts, type="choice", choices=choices)
664 # FIXME: there are many other currently unused completion types,
665 # should be added on an as-needed basis
666 raise Exception("Unhandled option kind '%s'" % kind)
669 #: serialised kind to arg type
671 "choices": cli.ArgChoice,
672 "command": cli.ArgCommand,
675 "jobid": cli.ArgJobId,
676 "onegroup": cli.ArgGroup,
677 "oneinstance": cli.ArgInstance,
678 "onenode": cli.ArgNode,
680 "string": cli.ArgUnknown,
681 "suggests": cli.ArgSuggest,
685 def HaskellArgToCliArg(kind, min_cnt, max_cnt):
686 """Converts a Haskell options to Python _Argument.
689 @param kind: type generated by Common.hs/argComplToText; needs to be
693 min_cnt = int(min_cnt)
694 if max_cnt == "none":
697 max_cnt = int(max_cnt)
698 # pylint: disable=W0142
699 # since we pass **kwargs
700 kwargs = {"min": min_cnt, "max": max_cnt}
702 if kind.startswith("choices=") or kind.startswith("suggest="):
703 (kind, choices) = kind.split("=", 1)
704 kwargs["choices"] = choices.split(",")
706 if kind not in _ARG_MAP:
707 raise Exception("Unhandled argument kind '%s'" % kind)
709 return _ARG_MAP[kind](**kwargs)
712 def WriteHaskellCompletion(sw, script, htools=True, debug=True):
713 """Generates completion information for a Haskell program.
715 This Converts completion info from a Haskell program into 'fake'
716 cli_opts and then builds completion for them.
720 cmd = "./htools/htools"
721 env = {"HTOOLS": script}
723 func_name = "htools_%s" % script
727 script_name = os.path.basename(script)
728 func_name = script_name
729 func_name = func_name.replace("-", "_")
730 output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
733 for line in output.splitlines():
735 exc = lambda msg: Exception("Invalid %s output from %s: %s" %
738 raise exc("help completion")
739 if v[0].startswith("-"):
741 raise exc("option format")
743 cli_opts.append(HaskellOptToOptParse(opts, kind))
746 raise exc("argument format")
747 (kind, min_cnt, max_cnt) = v
748 args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
749 WriteCompletion(sw, script_name, func_name, debug, opts=cli_opts, args=args)
753 parser = optparse.OptionParser(usage="%prog [--compact]")
754 parser.add_option("--compact", action="store_true",
755 help=("Don't indent output and don't include debugging"
758 options, args = parser.parse_args()
760 parser.error("Wrong number of arguments")
763 sw = utils.ShellWriter(buf, indent=not options.compact)
765 # Remember original state of extglob and enable it (required for pattern
766 # matching; must be enabled while parsing script)
767 sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
768 sw.Write("shopt -s extglob")
770 WritePreamble(sw, not options.compact)
773 for scriptname in _autoconf.GNT_SCRIPTS:
774 filename = "scripts/%s" % scriptname
776 WriteCompletion(sw, scriptname, GetFunctionName(scriptname),
778 commands=GetCommands(filename,
779 build.LoadModule(filename)))
782 burnin = build.LoadModule("tools/burnin")
783 WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
785 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
788 WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
789 debug=not options.compact)
793 for script in _autoconf.HTOOLS_PROGS:
794 WriteHaskellCompletion(sw, script, htools=True,
795 debug=not options.compact)
797 # ganeti-confd, if enabled
798 if _autoconf.ENABLE_CONFD:
799 WriteHaskellCompletion(sw, "htools/ganeti-confd", htools=False,
800 debug=not options.compact)
802 # Reset extglob to original value
803 sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
804 sw.Write("unset gnt_shopt_extglob")
809 if __name__ == "__main__":