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 from ganeti.tools import burnin
44 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
45 # making an exception here because this script is only used at build time.
46 from ganeti import _autoconf
48 #: Regular expression describing desired format of option names. Long names can
49 #: contain lowercase characters, numbers and dashes only.
50 _OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
53 def WritePreamble(sw, support_debug):
54 """Writes the script preamble.
56 Helper functions should be written here.
59 sw.Write("# This script is automatically generated at build time.")
60 sw.Write("# Do not modify manually.")
63 sw.Write("_gnt_log() {")
66 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
73 sw.Write("echo \"$@\"")
77 sw.Write("} >> $GANETI_COMPL_LOG")
85 sw.Write("_ganeti_nodes() {")
88 node_list_path = os.path.join(pathutils.DATA_DIR, "ssconf_node_list")
89 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
94 sw.Write("_ganeti_instances() {")
97 instance_list_path = os.path.join(pathutils.DATA_DIR,
98 "ssconf_instance_list")
99 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
104 sw.Write("_ganeti_jobs() {")
107 # FIXME: this is really going into the internals of the job queue
108 sw.Write(("local jlist=($( shopt -s nullglob &&"
109 " cd %s 2>/dev/null && echo job-* || : ))"),
110 utils.ShellQuote(pathutils.QUEUE_DIR))
111 sw.Write('echo "${jlist[@]/job-/}"')
116 for (fnname, paths) in [
117 ("os", pathutils.OS_SEARCH_PATH),
118 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
120 sw.Write("_ganeti_%s() {", fnname)
123 # FIXME: Make querying the master for all OSes cheap
125 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
126 utils.ShellQuote(path))
131 sw.Write("_ganeti_nodegroup() {")
134 nodegroups_path = os.path.join(pathutils.DATA_DIR, "ssconf_nodegroups")
135 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
140 sw.Write("_ganeti_network() {")
143 networks_path = os.path.join(pathutils.DATA_DIR, "ssconf_networks")
144 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
149 # Params: <offset> <options with values> <options without values>
150 # Result variable: $first_arg_idx
151 sw.Write("_ganeti_find_first_arg() {")
154 sw.Write("local w i")
156 sw.Write("first_arg_idx=")
157 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
160 sw.Write("w=${COMP_WORDS[$i]}")
163 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
166 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
168 # Ah, we found the first argument
169 sw.Write("else first_arg_idx=$i; break;")
178 # Params: <list of options separated by space>
179 # Input variable: $first_arg_idx
180 # Result variables: $arg_idx, $choices
181 sw.Write("_ganeti_list_options() {")
184 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
187 sw.Write("arg_idx=0")
188 # Show options only if the current word starts with a dash
189 sw.Write("""if [[ "$cur" == -* ]]; then""")
192 sw.Write("choices=$1")
201 # Calculate position of current argument
202 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
208 # Params: <long options with equal sign> <all options>
209 # Result variable: $optcur
210 sw.Write("_gnt_checkopt() {")
213 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
216 sw.Write("optcur=\"${cur#--*=}\"")
220 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
223 sw.Write("optcur=\"$cur\"")
230 sw.Write("_gnt_log optcur=\"'$optcur'\"")
237 # Params: <compgen options>
238 # Result variable: $COMPREPLY
239 sw.Write("_gnt_compgen() {")
242 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
244 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
250 def WriteCompReply(sw, args, cur="\"$cur\""):
251 sw.Write("_gnt_compgen %s -- %s", args, cur)
255 class CompletionWriter:
256 """Command completion writer class.
259 def __init__(self, arg_offset, opts, args, support_debug):
260 self.arg_offset = arg_offset
263 self.support_debug = support_debug
266 # While documented, these variables aren't seen as public attributes by
267 # pylint. pylint: disable=W0212
268 opt.all_names = sorted(opt._short_opts + opt._long_opts)
270 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
272 raise Exception("Option names don't match regular expression '%s': %s" %
273 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
275 def _FindFirstArgument(self, sw):
279 for opt in self.opts:
280 if opt.takes_value():
282 for i in opt.all_names:
283 if i.startswith("--"):
284 ignore.append("%s=*" % utils.ShellQuote(i))
285 skip_one.append(utils.ShellQuote(i))
287 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
289 ignore = sorted(utils.UniqueSequence(ignore))
290 skip_one = sorted(utils.UniqueSequence(skip_one))
292 if ignore or skip_one:
293 # Try to locate first argument
294 sw.Write("_ganeti_find_first_arg %s %s %s",
296 utils.ShellQuote("|".join(skip_one)),
297 utils.ShellQuote("|".join(ignore)))
299 # When there are no options the first argument is always at position
301 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
303 def _CompleteOptionValues(self, sw):
305 # "values" -> [optname1, optname2, ...]
308 for opt in self.opts:
309 if not opt.takes_value():
312 # Only static choices implemented so far (e.g. no node list)
313 suggest = getattr(opt, "completion_suggest", None)
315 # our custom option type
316 if opt.type == "bool":
317 suggest = ["yes", "no"]
320 suggest = opt.choices
322 if (isinstance(suggest, (int, long)) and
323 suggest in cli.OPT_COMPL_ALL):
326 key = " ".join(sorted(suggest))
330 values.setdefault(key, []).extend(opt.all_names)
332 # Don't write any code if there are no option values
340 for (suggest, allnames) in values.items():
341 longnames = [i for i in allnames if i.startswith("--")]
348 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
349 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
350 utils.ShellQuote("|".join(allnames)))
353 if suggest == cli.OPT_COMPL_MANY_NODES:
354 # TODO: Implement comma-separated values
355 WriteCompReply(sw, "-W ''", cur=cur)
356 elif suggest == cli.OPT_COMPL_ONE_NODE:
357 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
358 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
359 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
360 elif suggest == cli.OPT_COMPL_ONE_OS:
361 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
362 elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
363 WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
364 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
365 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
366 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
367 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
368 elif suggest == cli.OPT_COMPL_ONE_NETWORK:
369 WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
370 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
371 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
373 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
376 sw.Write("node1=\"${optcur%%:*}\"")
378 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
381 sw.Write("pfx=\"$node1:\"")
389 if self.support_debug:
390 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
391 " node1=\"'$node1'\"")
393 sw.Write("for i in $(_ganeti_nodes); do")
396 sw.Write("if [[ -z \"$node1\" ]]; then")
399 sw.Write("tmp=\"$tmp $i $i:\"")
402 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
405 sw.Write("tmp=\"$tmp $i\"")
413 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
415 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
426 def _CompleteArguments(self, sw):
427 if not (self.opts or self.args):
430 all_option_names = []
431 for opt in self.opts:
432 all_option_names.extend(opt.all_names)
433 all_option_names.sort()
435 # List options if no argument has been specified yet
436 sw.Write("_ganeti_list_options %s",
437 utils.ShellQuote(" ".join(all_option_names)))
440 last_idx = len(self.args) - 1
442 varlen_arg_idx = None
445 sw.Write("compgenargs=")
447 for idx, arg in enumerate(self.args):
448 assert arg.min is not None and arg.min >= 0
449 assert not (idx < last_idx and arg.max is None)
451 if arg.min != arg.max or arg.max is None:
452 if varlen_arg_idx is not None:
453 raise Exception("Only one argument can have a variable length")
458 if isinstance(arg, cli.ArgUnknown):
460 elif isinstance(arg, cli.ArgSuggest):
461 choices = utils.ShellQuote(" ".join(arg.choices))
462 elif isinstance(arg, cli.ArgInstance):
463 choices = "$(_ganeti_instances)"
464 elif isinstance(arg, cli.ArgNode):
465 choices = "$(_ganeti_nodes)"
466 elif isinstance(arg, cli.ArgGroup):
467 choices = "$(_ganeti_nodegroup)"
468 elif isinstance(arg, cli.ArgNetwork):
469 choices = "$(_ganeti_network)"
470 elif isinstance(arg, cli.ArgJobId):
471 choices = "$(_ganeti_jobs)"
472 elif isinstance(arg, cli.ArgOs):
473 choices = "$(_ganeti_os)"
474 elif isinstance(arg, cli.ArgExtStorage):
475 choices = "$(_ganeti_extstorage)"
476 elif isinstance(arg, cli.ArgFile):
478 compgenargs.append("-f")
479 elif isinstance(arg, cli.ArgCommand):
481 compgenargs.append("-c")
482 elif isinstance(arg, cli.ArgHost):
484 compgenargs.append("-A hostname")
486 raise Exception("Unknown argument type %r" % arg)
488 if arg.min == 1 and arg.max == 1:
489 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
490 elif arg.max is None:
491 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
492 elif arg.min <= arg.max:
493 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
494 (last_arg_end, last_arg_end + arg.max))
496 raise Exception("Unable to generate argument position condition")
498 last_arg_end += arg.min
500 if choices or compgenargs:
506 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
510 sw.Write("""choices="$choices "%s""", choices)
512 sw.Write("compgenargs=%s",
513 utils.ShellQuote(" ".join(compgenargs)))
523 WriteCompReply(sw, """-W "$choices" $compgenargs""")
525 # $compgenargs exists only if there are arguments
526 WriteCompReply(sw, '-W "$choices"')
528 def WriteTo(self, sw):
529 self._FindFirstArgument(sw)
530 self._CompleteOptionValues(sw)
531 self._CompleteArguments(sw)
534 def WriteCompletion(sw, scriptname, funcname, support_debug,
536 opts=None, args=None):
537 """Writes the completion code for one command.
539 @type sw: ShellWriter
540 @param sw: Script writer
541 @type scriptname: string
542 @param scriptname: Name of command line program
543 @type funcname: string
544 @param funcname: Shell function name
546 @param commands: List of all subcommands in this program
549 sw.Write("%s() {", funcname)
553 ' cur="${COMP_WORDS[COMP_CWORD]}"'
554 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
555 ' i first_arg_idx choices compgenargs arg_idx optcur')
558 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
559 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
560 " _gnt_log \"$(set | grep ^COMP_)\"")
562 sw.Write("COMPREPLY=()")
564 if opts is not None and args is not None:
566 CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
569 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
572 # Complete the command name
575 utils.ShellQuote(" ".join(sorted(commands.keys())))))
580 # Group commands by arguments and options
582 for cmd, (_, argdef, optdef, _, _) in commands.items():
583 if not (argdef or optdef):
585 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
587 # We're doing options and arguments to commands
588 sw.Write("""case "${COMP_WORDS[1]}" in""")
589 sort_grouped = sorted(grouped_cmds.items(),
590 key=lambda (_, y): sorted(y)[0])
591 for ((argdef, optdef), cmds) in sort_grouped:
592 assert argdef or optdef
593 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
596 CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
605 sw.Write("complete -F %s -o filenames %s",
606 utils.ShellQuote(funcname),
607 utils.ShellQuote(scriptname))
610 def GetFunctionName(name):
611 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
614 def GetCommands(filename, module):
615 """Returns the commands defined in a module.
617 Aliases are also added as commands.
621 commands = getattr(module, "commands")
622 except AttributeError:
623 raise Exception("Script %s doesn't have 'commands' attribute" %
626 # Add the implicit "--help" option
627 help_option = cli.cli_option("-h", "--help", default=False,
630 for name, (_, _, optdef, _, _) in commands.items():
631 if help_option not in optdef:
632 optdef.append(help_option)
633 for opt in cli.COMMON_OPTS:
635 raise Exception("Common option '%s' listed for command '%s' in %s" %
636 (opt, name, filename))
640 aliases = getattr(module, "aliases", {})
642 commands = commands.copy()
643 for name, target in aliases.items():
644 commands[name] = commands[target]
649 def HaskellOptToOptParse(opts, kind):
650 """Converts a Haskell options to Python cli_options.
653 @param opts: comma-separated string with short and long options
655 @param kind: type generated by Common.hs/complToText; needs to be
659 # pylint: disable=W0142
660 # since we pass *opts in a number of places
661 opts = opts.split(",")
663 return cli.cli_option(*opts, action="store_true")
664 elif kind in ["file", "string", "host", "dir", "inetaddr"]:
665 return cli.cli_option(*opts, type="string")
666 elif kind == "integer":
667 return cli.cli_option(*opts, type="int")
668 elif kind == "float":
669 return cli.cli_option(*opts, type="float")
670 elif kind == "onegroup":
671 return cli.cli_option(*opts, type="string",
672 completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
673 elif kind == "onenode":
674 return cli.cli_option(*opts, type="string",
675 completion_suggest=cli.OPT_COMPL_ONE_NODE)
676 elif kind == "manyinstances":
677 # FIXME: no support for many instances
678 return cli.cli_option(*opts, type="string")
679 elif kind.startswith("choices="):
680 choices = kind[len("choices="):].split(",")
681 return cli.cli_option(*opts, type="choice", choices=choices)
683 # FIXME: there are many other currently unused completion types,
684 # should be added on an as-needed basis
685 raise Exception("Unhandled option kind '%s'" % kind)
688 #: serialised kind to arg type
690 "choices": cli.ArgChoice,
691 "command": cli.ArgCommand,
694 "jobid": cli.ArgJobId,
695 "onegroup": cli.ArgGroup,
696 "oneinstance": cli.ArgInstance,
697 "onenode": cli.ArgNode,
699 "string": cli.ArgUnknown,
700 "suggests": cli.ArgSuggest,
704 def HaskellArgToCliArg(kind, min_cnt, max_cnt):
705 """Converts a Haskell options to Python _Argument.
708 @param kind: type generated by Common.hs/argComplToText; needs to be
712 min_cnt = int(min_cnt)
713 if max_cnt == "none":
716 max_cnt = int(max_cnt)
717 # pylint: disable=W0142
718 # since we pass **kwargs
719 kwargs = {"min": min_cnt, "max": max_cnt}
721 if kind.startswith("choices=") or kind.startswith("suggest="):
722 (kind, choices) = kind.split("=", 1)
723 kwargs["choices"] = choices.split(",")
725 if kind not in _ARG_MAP:
726 raise Exception("Unhandled argument kind '%s'" % kind)
728 return _ARG_MAP[kind](**kwargs)
731 def ParseHaskellOptsArgs(script, output):
732 """Computes list of options/arguments from help-completion output.
737 for line in output.splitlines():
739 exc = lambda msg: Exception("Invalid %s output from %s: %s" %
742 raise exc("help completion")
743 if v[0].startswith("-"):
745 raise exc("option format")
747 cli_opts.append(HaskellOptToOptParse(opts, kind))
750 raise exc("argument format")
751 (kind, min_cnt, max_cnt) = v
752 cli_args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
753 return (cli_opts, cli_args)
756 def WriteHaskellCompletion(sw, script, htools=True, debug=True):
757 """Generates completion information for a Haskell program.
759 This converts completion info from a Haskell program into 'fake'
760 cli_opts and then builds completion for them.
765 env = {"HTOOLS": script}
767 func_name = "htools_%s" % script
771 script_name = os.path.basename(script)
772 func_name = script_name
773 func_name = GetFunctionName(func_name)
774 output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
775 (opts, args) = ParseHaskellOptsArgs(script_name, output)
776 WriteCompletion(sw, script_name, func_name, debug, opts=opts, args=args)
779 def WriteHaskellCmdCompletion(sw, script, debug=True):
780 """Generates completion information for a Haskell multi-command program.
782 This gathers the list of commands from a Haskell program and
783 computes the list of commands available, then builds the sub-command
784 list of options/arguments for each command, using that for building
785 a unified help output.
789 script_name = os.path.basename(script)
790 func_name = script_name
791 func_name = GetFunctionName(func_name)
792 output = utils.RunCmd([cmd, "--help-completion"], cwd=".").output
794 lines = output.splitlines()
796 raise Exception("Invalid lines in multi-command mode: %s" % str(lines))
797 v = lines[0].split(None)
798 exc = lambda msg: Exception("Invalid %s output from %s: %s" %
801 raise exc("help completion in multi-command mode")
802 if not v[0].startswith("choices="):
803 raise exc("invalid format in multi-command mode '%s'" % v[0])
804 for subcmd in v[0][len("choices="):].split(","):
805 output = utils.RunCmd([cmd, subcmd, "--help-completion"], cwd=".").output
806 (opts, args) = ParseHaskellOptsArgs(script, output)
807 commands[subcmd] = (None, args, opts, None, None)
808 WriteCompletion(sw, script_name, func_name, debug, commands=commands)
812 parser = optparse.OptionParser(usage="%prog [--compact]")
813 parser.add_option("--compact", action="store_true",
814 help=("Don't indent output and don't include debugging"
817 options, args = parser.parse_args()
819 parser.error("Wrong number of arguments")
821 # Whether to build debug version of completion script
822 debug = not options.compact
825 sw = utils.ShellWriter(buf, indent=debug)
827 # Remember original state of extglob and enable it (required for pattern
828 # matching; must be enabled while parsing script)
829 sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
830 sw.Write("shopt -s extglob")
832 WritePreamble(sw, debug)
835 for scriptname in _autoconf.GNT_SCRIPTS:
836 filename = "scripts/%s" % scriptname
838 WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
839 commands=GetCommands(filename,
840 build.LoadModule(filename)))
843 WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
845 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
848 WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
849 debug=not options.compact)
853 for script in _autoconf.HTOOLS_PROGS:
854 WriteHaskellCompletion(sw, script, htools=True, debug=debug)
856 # ganeti-confd, if enabled
857 if _autoconf.ENABLE_CONFD:
858 WriteHaskellCompletion(sw, "src/ganeti-confd", htools=False,
861 # mon-collector, if monitoring is enabled
862 if _autoconf.ENABLE_MOND:
863 WriteHaskellCmdCompletion(sw, "src/mon-collector", debug=debug)
865 # Reset extglob to original value
866 sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
867 sw.Write("unset gnt_shopt_extglob")
872 if __name__ == "__main__":