4 # Copyright (C) 2009 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-msg=C0103
27 # [C0103] Invalid name build-bash-completion
31 from cStringIO import StringIO
33 from ganeti import constants
34 from ganeti import cli
35 from ganeti import utils
36 from ganeti import build
38 # _autoconf shouldn't be imported from anywhere except constants.py, but we're
39 # making an exception here because this script is only used at build time.
40 from ganeti import _autoconf
44 """Helper class to write scripts with indentation.
49 def __init__(self, fh):
54 """Increase indentation level by 1.
60 """Decrease indentation level by 1.
63 assert self._indent > 0
66 def Write(self, txt, *args):
67 """Write line to output file.
70 self._fh.write(self._indent * self.INDENT_STR)
73 self._fh.write(txt % args)
80 def WritePreamble(sw):
81 """Writes the script preamble.
83 Helper functions should be written here.
86 sw.Write("# This script is automatically generated at build time.")
87 sw.Write("# Do not modify manually.")
89 sw.Write("_ganeti_dbglog() {")
92 sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
99 sw.Write("echo \"$@\"")
103 sw.Write("} >> $GANETI_COMPL_LOG")
111 sw.Write("_ganeti_nodes() {")
114 node_list_path = os.path.join(constants.DATA_DIR, "ssconf_node_list")
115 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(node_list_path))
120 sw.Write("_ganeti_instances() {")
123 instance_list_path = os.path.join(constants.DATA_DIR,
124 "ssconf_instance_list")
125 sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(instance_list_path))
130 sw.Write("_ganeti_jobs() {")
133 # FIXME: this is really going into the internals of the job queue
134 sw.Write(("local jlist=$( shopt -s nullglob &&"
135 " cd %s 2>/dev/null && echo job-* || : )"),
136 utils.ShellQuote(constants.QUEUE_DIR))
137 sw.Write('echo "${jlist//job-/}"')
142 for (fnname, paths) in [
143 ("os", constants.OS_SEARCH_PATH),
144 ("iallocator", constants.IALLOCATOR_SEARCH_PATH),
146 sw.Write("_ganeti_%s() {", fnname)
149 # FIXME: Make querying the master for all OSes cheap
151 sw.Write("( shopt -s nullglob && cd %s 2>/dev/null && echo * || : )",
152 utils.ShellQuote(path))
157 # Params: <offset> <options with values> <options without values>
158 # Result variable: $first_arg_idx
159 sw.Write("_ganeti_find_first_arg() {")
162 sw.Write("local w i")
164 sw.Write("first_arg_idx=")
165 sw.Write("for (( i=$1; i < COMP_CWORD; ++i )); do")
168 sw.Write("w=${COMP_WORDS[$i]}")
171 sw.Write("""if [[ -n "$2" && "$w" == @($2) ]]; then let ++i""")
174 sw.Write("""elif [[ -n "$3" && "$w" == @($3) ]]; then :""")
176 # Ah, we found the first argument
177 sw.Write("else first_arg_idx=$i; break;")
186 # Params: <list of options separated by space>
187 # Input variable: $first_arg_idx
188 # Result variables: $arg_idx, $choices
189 sw.Write("_ganeti_list_options() {")
192 sw.Write("""if [[ -z "$first_arg_idx" ]]; then""")
195 sw.Write("arg_idx=0")
196 # Show options only if the current word starts with a dash
197 sw.Write("""if [[ "$cur" == -* ]]; then""")
200 sw.Write("choices=$1")
209 # Calculate position of current argument
210 sw.Write("arg_idx=$(( COMP_CWORD - first_arg_idx ))")
216 # Params: <long options with equal sign> <all options>
217 # Result variable: $optcur
218 sw.Write("_ganeti_checkopt() {")
221 sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
224 sw.Write("optcur=\"${cur#--*=}\"")
228 sw.Write("""elif [[ -n "$2" && "$prev" == @($2) ]]; then""")
231 sw.Write("optcur=\"$cur\"")
237 sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
244 # Params: <compgen options>
245 # Result variable: $COMPREPLY
246 sw.Write("_ganeti_compgen() {")
249 sw.Write("""COMPREPLY=( $(compgen "$@") )""")
250 sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
256 def WriteCompReply(sw, args, cur="\"$cur\""):
257 sw.Write("_ganeti_compgen %s -- %s", args, cur)
261 class CompletionWriter:
262 """Command completion writer class.
265 def __init__(self, arg_offset, opts, args):
266 self.arg_offset = arg_offset
271 # While documented, these variables aren't seen as public attributes by
272 # pylint. pylint: disable-msg=W0212
273 opt.all_names = sorted(opt._short_opts + opt._long_opts)
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.iteritems():
341 longnames = [i for i in allnames if i.startswith("--")]
348 sw.Write("%s _ganeti_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_IALLOCATOR:
363 WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
364 elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
365 sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
367 sw.Write("if [[ \"$optcur\" == *:* ]]; then")
370 sw.Write("node1=\"${optcur%%:*}\"")
372 sw.Write("if [[ \"$COMP_WORDBREAKS\" != *:* ]]; then")
375 sw.Write("pfx=\"$node1:\"")
383 sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
384 " node1=\"'$node1'\"")
386 sw.Write("for i in $(_ganeti_nodes); do")
389 sw.Write("if [[ -z \"$node1\" ]]; then")
392 sw.Write("tmp=\"$tmp $i $i:\"")
395 sw.Write("elif [[ \"$i\" != \"$node1\" ]]; then")
398 sw.Write("tmp=\"$tmp $i\"")
406 WriteCompReply(sw, "-P \"$pfx\" -W \"$tmp\"", cur="\"$curvalue\"")
408 WriteCompReply(sw, "-W %s" % utils.ShellQuote(suggest), cur=cur)
419 def _CompleteArguments(self, sw):
420 if not (self.opts or self.args):
423 all_option_names = []
424 for opt in self.opts:
425 all_option_names.extend(opt.all_names)
426 all_option_names.sort()
428 # List options if no argument has been specified yet
429 sw.Write("_ganeti_list_options %s",
430 utils.ShellQuote(" ".join(all_option_names)))
433 last_idx = len(self.args) - 1
435 varlen_arg_idx = None
438 # Write some debug comments
439 for idx, arg in enumerate(self.args):
440 sw.Write("# %s: %r", idx, arg)
442 sw.Write("compgenargs=")
444 for idx, arg in enumerate(self.args):
445 assert arg.min is not None and arg.min >= 0
446 assert not (idx < last_idx and arg.max is None)
448 if arg.min != arg.max or arg.max is None:
449 if varlen_arg_idx is not None:
450 raise Exception("Only one argument can have a variable length")
455 if isinstance(arg, cli.ArgUnknown):
457 elif isinstance(arg, cli.ArgSuggest):
458 choices = utils.ShellQuote(" ".join(arg.choices))
459 elif isinstance(arg, cli.ArgInstance):
460 choices = "$(_ganeti_instances)"
461 elif isinstance(arg, cli.ArgNode):
462 choices = "$(_ganeti_nodes)"
463 elif isinstance(arg, cli.ArgJobId):
464 choices = "$(_ganeti_jobs)"
465 elif isinstance(arg, cli.ArgOs):
466 choices = "$(_ganeti_os)"
467 elif isinstance(arg, cli.ArgFile):
469 compgenargs.append("-f")
470 elif isinstance(arg, cli.ArgCommand):
472 compgenargs.append("-c")
473 elif isinstance(arg, cli.ArgHost):
475 compgenargs.append("-A hostname")
477 raise Exception("Unknown argument type %r" % arg)
479 if arg.min == 1 and arg.max == 1:
480 cmpcode = """"$arg_idx" == %d""" % (last_arg_end)
481 elif arg.max is None:
482 cmpcode = """"$arg_idx" -ge %d""" % (last_arg_end)
483 elif arg.min <= arg.max:
484 cmpcode = (""""$arg_idx" -ge %d && "$arg_idx" -lt %d""" %
485 (last_arg_end, last_arg_end + arg.max))
487 raise Exception("Unable to generate argument position condition")
489 last_arg_end += arg.min
491 if choices or compgenargs:
497 sw.Write("""%s [[ %s ]]; then""", condcmd, cmpcode)
501 sw.Write("""choices="$choices "%s""", choices)
503 sw.Write("compgenargs=%s",
504 utils.ShellQuote(" ".join(compgenargs)))
514 WriteCompReply(sw, """-W "$choices" $compgenargs""")
516 # $compgenargs exists only if there are arguments
517 WriteCompReply(sw, '-W "$choices"')
519 def WriteTo(self, sw):
520 self._FindFirstArgument(sw)
521 self._CompleteOptionValues(sw)
522 self._CompleteArguments(sw)
525 def WriteCompletion(sw, scriptname, funcname,
527 opts=None, args=None):
528 """Writes the completion code for one command.
530 @type sw: ShellWriter
531 @param sw: Script writer
532 @type scriptname: string
533 @param scriptname: Name of command line program
534 @type funcname: string
535 @param funcname: Shell function name
537 @param commands: List of all subcommands in this program
540 sw.Write("%s() {", funcname)
544 ' cur="${COMP_WORDS[COMP_CWORD]}"'
545 ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
546 ' i first_arg_idx choices compgenargs arg_idx optcur')
548 sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
549 sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
550 " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
552 sw.Write("COMPREPLY=()")
554 if opts is not None and args is not None:
556 CompletionWriter(0, opts, args).WriteTo(sw)
559 sw.Write("""if [[ "$COMP_CWORD" == 1 ]]; then""")
562 # Complete the command name
565 utils.ShellQuote(" ".join(sorted(commands.keys())))))
570 # We're doing options and arguments to commands
571 sw.Write("""case "${COMP_WORDS[1]}" in""")
572 for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
573 if not (argdef or optdef):
576 # TODO: Group by arguments and options
577 sw.Write("%s)", utils.ShellQuote(cmd))
580 CompletionWriter(1, optdef, argdef).WriteTo(sw)
590 sw.Write("complete -F %s -o filenames %s",
591 utils.ShellQuote(funcname),
592 utils.ShellQuote(scriptname))
595 def GetFunctionName(name):
596 return "_" + re.sub(r"[^a-z0-9]+", "_", name.lower())
599 def GetCommands(filename, module):
600 """Returns the commands defined in a module.
602 Aliases are also added as commands.
606 commands = getattr(module, "commands")
607 except AttributeError:
608 raise Exception("Script %s doesn't have 'commands' attribute" %
611 # Add the implicit "--help" option
612 help_option = cli.cli_option("-h", "--help", default=False,
615 for (_, _, optdef, _, _) in commands.itervalues():
616 if help_option not in optdef:
617 optdef.append(help_option)
618 if cli.DEBUG_OPT not in optdef:
619 optdef.append(cli.DEBUG_OPT)
622 aliases = getattr(module, "aliases", {})
624 commands = commands.copy()
625 for name, target in aliases.iteritems():
626 commands[name] = commands[target]
633 sw = ShellWriter(buf)
638 for scriptname in _autoconf.GNT_SCRIPTS:
639 filename = "scripts/%s" % scriptname
641 WriteCompletion(sw, scriptname,
642 GetFunctionName(scriptname),
643 commands=GetCommands(filename,
644 build.LoadModule(filename)))
647 burnin = build.LoadModule("tools/burnin")
648 WriteCompletion(sw, "%s/burnin" % constants.TOOLSDIR, "_ganeti_burnin",
649 opts=burnin.OPTIONS, args=burnin.ARGUMENTS)
654 if __name__ == "__main__":