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 sw.Write("_ganeti_network() {")
141 networks_path = os.path.join(pathutils.DATA_DIR, "ssconf_networks")
142 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(networks_path))
147 # Params: <offset> <options with values> <options without values>
148 # Result variable: $first_arg_idx
149 sw.Write("_ganeti_find_first_arg() {")
152 sw.Write("local w i")
154 sw.Write("first_arg_idx=")
155 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
158 sw.Write("w=${COMP_WORDS[$i]}")
161 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
164 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
166 # Ah, we found the first argument
167 sw.Write("else first_arg_idx=$i; break;")
176 # Params: <list of options separated by space>
177 # Input variable: $first_arg_idx
178 # Result variables: $arg_idx, $choices
179 sw.Write("_ganeti_list_options() {")
182 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
185 sw.Write("arg_idx=0")
186 # Show options only if the current word starts with a dash
187 sw.Write("""if [[ "$cur" == -* ]]; then""")
190 sw.Write("choices=$1")
199 # Calculate position of current argument
200 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
206 # Params: <long options with equal sign> <all options>
207 # Result variable: $optcur
208 sw.Write("_gnt_checkopt() {")
211 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
214 sw.Write("optcur=\"${cur#--*=}\"")
218 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
221 sw.Write("optcur=\"$cur\"")
228 sw.Write("_gnt_log optcur=\"'$optcur'\"")
235 # Params: <compgen options>
236 # Result variable: $COMPREPLY
237 sw.Write("_gnt_compgen() {")
240 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
242 sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
248 def WriteCompReply(sw, args, cur="\"$cur\""):
249 sw.Write("_gnt_compgen %s -- %s", args, cur)
253 class CompletionWriter:
254 """Command completion writer class.
257 def __init__(self, arg_offset, opts, args, support_debug):
258 self.arg_offset = arg_offset
261 self.support_debug = support_debug
264 # While documented, these variables aren't seen as public attributes by
265 # pylint. pylint: disable=W0212
266 opt.all_names = sorted(opt._short_opts + opt._long_opts)
268 invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
270 raise Exception("Option names don't match regular expression '%s': %s" %
271 (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
273 def _FindFirstArgument(self, sw):
277 for opt in self.opts:
278 if opt.takes_value():
280 for i in opt.all_names:
281 if i.startswith("--"):
282 ignore.append("%s=*" % utils.ShellQuote(i))
283 skip_one.append(utils.ShellQuote(i))
285 ignore.extend([utils.ShellQuote(i) for i in opt.all_names])
287 ignore = sorted(utils.UniqueSequence(ignore))
288 skip_one = sorted(utils.UniqueSequence(skip_one))
290 if ignore or skip_one:
291 # Try to locate first argument
292 sw.Write("_ganeti_find_first_arg %s %s %s",
294 utils.ShellQuote("|".join(skip_one)),
295 utils.ShellQuote("|".join(ignore)))
297 # When there are no options the first argument is always at position
299 sw.Write("first_arg_idx=%s", self.arg_offset + 1)
301 def _CompleteOptionValues(self, sw):
303 # "values" -> [optname1, optname2, ...]
306 for opt in self.opts:
307 if not opt.takes_value():
310 # Only static choices implemented so far (e.g. no node list)
311 suggest = getattr(opt, "completion_suggest", None)
313 # our custom option type
314 if opt.type == "bool":
315 suggest = ["yes", "no"]
318 suggest = opt.choices
320 if (isinstance(suggest, (int, long)) and
321 suggest in cli.OPT_COMPL_ALL):
324 key = " ".join(sorted(suggest))
328 values.setdefault(key, []).extend(opt.all_names)
330 # Don't write any code if there are no option values
338 for (suggest, allnames) in values.items():
339 longnames = [i for i in allnames if i.startswith("--")]
346 sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
347 utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
348 utils.ShellQuote("|".join(allnames)))
351 if suggest == cli.OPT_COMPL_MANY_NODES:
352 # TODO: Implement comma-separated values
353 WriteCompReply(sw, "-W ''", cur=cur)
354 elif suggest == cli.OPT_COMPL_ONE_NODE:
355 WriteCompReply(sw, "-W \"$(_ganeti_nodes)\"", cur=cur)
356 elif suggest == cli.OPT_COMPL_ONE_INSTANCE:
357 WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
358 elif suggest == cli.OPT_COMPL_ONE_OS:
359 WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
360 elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
361 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
362 elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
363 WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
364 elif suggest == cli.OPT_COMPL_ONE_NETWORK:
365 WriteCompReply(sw, "-W \"$(_ganeti_network)\"", cur=cur)
366 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
367 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
369 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
372 sw.Write("node1=\"${optcur%%:*}\"")
374 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
377 sw.Write("pfx=\"$node1:\"")
385 if self.support_debug:
386 sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
387 " node1=\"'$node1'\"")
389 sw.Write("for i in $(_ganeti_nodes); do")
392 sw.Write("if [[ -z \"$node1\" ]]; then")
395 sw.Write("tmp=\"$tmp $i $i:\"")
398 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
401 sw.Write("tmp=\"$tmp $i\"")
409 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
411 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
422 def _CompleteArguments(self, sw):
423 if not (self.opts or self.args):
426 all_option_names = []
427 for opt in self.opts:
428 all_option_names.extend(opt.all_names)
429 all_option_names.sort()
431 # List options if no argument has been specified yet
432 sw.Write("_ganeti_list_options %s",
433 utils.ShellQuote(" ".join(all_option_names)))
436 last_idx = len(self.args) - 1
438 varlen_arg_idx = None
441 sw.Write("compgenargs=")
443 for idx, arg in enumerate(self.args):
444 assert arg.min is not None and arg.min >= 0
445 assert not (idx < last_idx and arg.max is None)
447 if arg.min != arg.max or arg.max is None:
448 if varlen_arg_idx is not None:
449 raise Exception("Only one argument can have a variable length")
454 if isinstance(arg, cli.ArgUnknown):
456 elif isinstance(arg, cli.ArgSuggest):
457 choices = utils.ShellQuote(" ".join(arg.choices))
458 elif isinstance(arg, cli.ArgInstance):
459 choices = "$(_ganeti_instances)"
460 elif isinstance(arg, cli.ArgNode):
461 choices = "$(_ganeti_nodes)"
462 elif isinstance(arg, cli.ArgGroup):
463 choices = "$(_ganeti_nodegroup)"
464 elif isinstance(arg, cli.ArgNetwork):
465 choices = "$(_ganeti_network)"
466 elif isinstance(arg, cli.ArgJobId):
467 choices = "$(_ganeti_jobs)"
468 elif isinstance(arg, cli.ArgOs):
469 choices = "$(_ganeti_os)"
470 elif isinstance(arg, cli.ArgFile):
472 compgenargs.append("-f")
473 elif isinstance(arg, cli.ArgCommand):
475 compgenargs.append("-c")
476 elif isinstance(arg, cli.ArgHost):
478 compgenargs.append("-A hostname")
480 raise Exception("Unknown argument type %r" % arg)
482 if arg.min == 1 and arg.max == 1:
483 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
484 elif arg.max is None:
485 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
486 elif arg.min <= arg.max:
487 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
488 (last_arg_end, last_arg_end + arg.max))
490 raise Exception("Unable to generate argument position condition")
492 last_arg_end += arg.min
494 if choices or compgenargs:
500 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
504 sw.Write("""choices="$choices "%s""", choices)
506 sw.Write("compgenargs=%s",
507 utils.ShellQuote(" ".join(compgenargs)))
517 WriteCompReply(sw, """-W "$choices" $compgenargs""")
519 # $compgenargs exists only if there are arguments
520 WriteCompReply(sw, '-W "$choices"')
522 def WriteTo(self, sw):
523 self._FindFirstArgument(sw)
524 self._CompleteOptionValues(sw)
525 self._CompleteArguments(sw)
528 def WriteCompletion(sw, scriptname, funcname, support_debug,
530 opts=None, args=None):
531 """Writes the completion code for one command.
533 @type sw: ShellWriter
534 @param sw: Script writer
535 @type scriptname: string
536 @param scriptname: Name of command line program
537 @type funcname: string
538 @param funcname: Shell function name
540 @param commands: List of all subcommands in this program
543 sw.Write("%s() {", funcname)
547 ' cur="${COMP_WORDS[COMP_CWORD]}"'
548 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
549 ' i first_arg_idx choices compgenargs arg_idx optcur')
552 sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
553 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
554 " _gnt_log \"$(set | grep ^COMP_)\"")
556 sw.Write("COMPREPLY=()")
558 if opts is not None and args is not None:
560 CompletionWriter(0, opts, args, support_debug).WriteTo(sw)
563 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
566 # Complete the command name
569 utils.ShellQuote(" ".join(sorted(commands.keys())))))
574 # Group commands by arguments and options
576 for cmd, (_, argdef, optdef, _, _) in commands.items():
577 if not (argdef or optdef):
579 grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
581 # We're doing options and arguments to commands
582 sw.Write("""case "${COMP_WORDS[1]}" in""")
583 sort_grouped = sorted(grouped_cmds.items(),
584 key=lambda (_, y): sorted(y)[0])
585 for ((argdef, optdef), cmds) in sort_grouped:
586 assert argdef or optdef
587 sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
590 CompletionWriter(1, optdef, argdef, support_debug).WriteTo(sw)
599 sw.Write("complete -F %s -o filenames %s",
600 utils.ShellQuote(funcname),
601 utils.ShellQuote(scriptname))
604 def GetFunctionName(name):
605 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
608 def GetCommands(filename, module):
609 """Returns the commands defined in a module.
611 Aliases are also added as commands.
615 commands = getattr(module, "commands")
616 except AttributeError:
617 raise Exception("Script %s doesn't have 'commands' attribute" %
620 # Add the implicit "--help" option
621 help_option = cli.cli_option("-h", "--help", default=False,
624 for name, (_, _, optdef, _, _) in commands.items():
625 if help_option not in optdef:
626 optdef.append(help_option)
627 for opt in cli.COMMON_OPTS:
629 raise Exception("Common option '%s' listed for command '%s' in %s" %
630 (opt, name, filename))
634 aliases = getattr(module, "aliases", {})
636 commands = commands.copy()
637 for name, target in aliases.items():
638 commands[name] = commands[target]
643 def HaskellOptToOptParse(opts, kind):
644 """Converts a Haskell options to Python cli_options.
647 @param opts: comma-separated string with short and long options
649 @param kind: type generated by Common.hs/complToText; needs to be
653 # pylint: disable=W0142
654 # since we pass *opts in a number of places
655 opts = opts.split(",")
657 return cli.cli_option(*opts, action="store_true")
658 elif kind in ["file", "string", "host", "dir", "inetaddr"]:
659 return cli.cli_option(*opts, type="string")
660 elif kind == "integer":
661 return cli.cli_option(*opts, type="int")
662 elif kind == "float":
663 return cli.cli_option(*opts, type="float")
664 elif kind == "onegroup":
665 return cli.cli_option(*opts, type="string",
666 completion_suggest=cli.OPT_COMPL_ONE_NODEGROUP)
667 elif kind == "onenode":
668 return cli.cli_option(*opts, type="string",
669 completion_suggest=cli.OPT_COMPL_ONE_NODE)
670 elif kind == "manyinstances":
671 # FIXME: no support for many instances
672 return cli.cli_option(*opts, type="string")
673 elif kind.startswith("choices="):
674 choices = kind[len("choices="):].split(",")
675 return cli.cli_option(*opts, type="choice", choices=choices)
677 # FIXME: there are many other currently unused completion types,
678 # should be added on an as-needed basis
679 raise Exception("Unhandled option kind '%s'" % kind)
682 #: serialised kind to arg type
684 "choices": cli.ArgChoice,
685 "command": cli.ArgCommand,
688 "jobid": cli.ArgJobId,
689 "onegroup": cli.ArgGroup,
690 "oneinstance": cli.ArgInstance,
691 "onenode": cli.ArgNode,
693 "string": cli.ArgUnknown,
694 "suggests": cli.ArgSuggest,
698 def HaskellArgToCliArg(kind, min_cnt, max_cnt):
699 """Converts a Haskell options to Python _Argument.
702 @param kind: type generated by Common.hs/argComplToText; needs to be
706 min_cnt = int(min_cnt)
707 if max_cnt == "none":
710 max_cnt = int(max_cnt)
711 # pylint: disable=W0142
712 # since we pass **kwargs
713 kwargs = {"min": min_cnt, "max": max_cnt}
715 if kind.startswith("choices=") or kind.startswith("suggest="):
716 (kind, choices) = kind.split("=", 1)
717 kwargs["choices"] = choices.split(",")
719 if kind not in _ARG_MAP:
720 raise Exception("Unhandled argument kind '%s'" % kind)
722 return _ARG_MAP[kind](**kwargs)
725 def ParseHaskellOptsArgs(script, output):
726 """Computes list of options/arguments from help-completion output.
731 for line in output.splitlines():
733 exc = lambda msg: Exception("Invalid %s output from %s: %s" %
736 raise exc("help completion")
737 if v[0].startswith("-"):
739 raise exc("option format")
741 cli_opts.append(HaskellOptToOptParse(opts, kind))
744 raise exc("argument format")
745 (kind, min_cnt, max_cnt) = v
746 cli_args.append(HaskellArgToCliArg(kind, min_cnt, max_cnt))
747 return (cli_opts, cli_args)
750 def WriteHaskellCompletion(sw, script, htools=True, debug=True):
751 """Generates completion information for a Haskell program.
753 This converts completion info from a Haskell program into 'fake'
754 cli_opts and then builds completion for them.
758 cmd = "./htools/htools"
759 env = {"HTOOLS": script}
761 func_name = "htools_%s" % script
765 script_name = os.path.basename(script)
766 func_name = script_name
767 func_name = GetFunctionName(func_name)
768 output = utils.RunCmd([cmd, "--help-completion"], env=env, cwd=".").output
769 (opts, args) = ParseHaskellOptsArgs(script_name, output)
770 WriteCompletion(sw, script_name, func_name, debug, opts=opts, args=args)
773 def WriteHaskellCmdCompletion(sw, script, debug=True):
774 """Generates completion information for a Haskell multi-command program.
776 This gathers the list of commands from a Haskell program and
777 computes the list of commands available, then builds the sub-command
778 list of options/arguments for each command, using that for building
779 a unified help output.
783 script_name = os.path.basename(script)
784 func_name = script_name
785 func_name = GetFunctionName(func_name)
786 output = utils.RunCmd([cmd, "--help-completion"], cwd=".").output
788 lines = output.splitlines()
790 raise Exception("Invalid lines in multi-command mode: %s" % str(lines))
791 v = lines[0].split(None)
792 exc = lambda msg: Exception("Invalid %s output from %s: %s" %
795 raise exc("help completion in multi-command mode")
796 if not v[0].startswith("choices="):
797 raise exc("invalid format in multi-command mode '%s'" % v[0])
798 for subcmd in v[0][len("choices="):].split(","):
799 output = utils.RunCmd([cmd, subcmd, "--help-completion"], cwd=".").output
800 (opts, args) = ParseHaskellOptsArgs(script, output)
801 commands[subcmd] = (None, args, opts, None, None)
802 WriteCompletion(sw, script_name, func_name, debug, commands=commands)
806 parser = optparse.OptionParser(usage="%prog [--compact]")
807 parser.add_option("--compact", action="store_true",
808 help=("Don't indent output and don't include debugging"
811 options, args = parser.parse_args()
813 parser.error("Wrong number of arguments")
815 # Whether to build debug version of completion script
816 debug = not options.compact
819 sw = utils.ShellWriter(buf, indent=debug)
821 # Remember original state of extglob and enable it (required for pattern
822 # matching; must be enabled while parsing script)
823 sw.Write("gnt_shopt_extglob=$(shopt -p extglob || :)")
824 sw.Write("shopt -s extglob")
826 WritePreamble(sw, debug)
829 for scriptname in _autoconf.GNT_SCRIPTS:
830 filename = "scripts/%s" % scriptname
832 WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
833 commands=GetCommands(filename,
834 build.LoadModule(filename)))
837 burnin = build.LoadModule("tools/burnin")
838 WriteCompletion(sw, "%s/burnin" % pathutils.TOOLSDIR, "_ganeti_burnin",
840 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
843 WriteHaskellCompletion(sw, "daemons/ganeti-cleaner", htools=False,
844 debug=not options.compact)
848 for script in _autoconf.HTOOLS_PROGS:
849 WriteHaskellCompletion(sw, script, htools=True, debug=debug)
851 # ganeti-confd, if enabled
852 if _autoconf.ENABLE_CONFD:
853 WriteHaskellCompletion(sw, "htools/ganeti-confd", htools=False,
856 # mon-collector, if monitoring is enabled
857 if _autoconf.ENABLE_MONITORING:
858 WriteHaskellCmdCompletion(sw, "htools/mon-collector", debug=debug)
860 # Reset extglob to original value
861 sw.Write("[[ -n \"$gnt_shopt_extglob\" ]] && $gnt_shopt_extglob")
862 sw.Write("unset gnt_shopt_extglob")
867 if __name__ == "__main__":