summaryrefslogtreecommitdiffstats
path: root/python/mach/mach/commands/commandinfo.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mach/mach/commands/commandinfo.py')
-rw-r--r--python/mach/mach/commands/commandinfo.py487
1 files changed, 487 insertions, 0 deletions
diff --git a/python/mach/mach/commands/commandinfo.py b/python/mach/mach/commands/commandinfo.py
new file mode 100644
index 0000000000..12c4b240ea
--- /dev/null
+++ b/python/mach/mach/commands/commandinfo.py
@@ -0,0 +1,487 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, # You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import re
+import subprocess
+import sys
+from itertools import chain
+from pathlib import Path
+
+import attr
+from mozbuild.util import memoize
+
+from mach.decorators import Command, CommandArgument, SubCommand
+
+COMPLETION_TEMPLATES_DIR = Path(__file__).resolve().parent / "completion_templates"
+
+
+@attr.s
+class CommandInfo(object):
+ name = attr.ib(type=str)
+ description = attr.ib(type=str)
+ subcommands = attr.ib(type=list)
+ options = attr.ib(type=dict)
+ subcommand = attr.ib(type=str, default=None)
+
+
+def render_template(shell, context):
+ filename = "{}.template".format(shell)
+ with open(COMPLETION_TEMPLATES_DIR / filename) as fh:
+ template = fh.read()
+ return template % context
+
+
+@memoize
+def command_handlers(command_context):
+ """A dictionary of command handlers keyed by command name."""
+ return command_context._mach_context.commands.command_handlers
+
+
+@memoize
+def commands(command_context):
+ """A sorted list of all command names."""
+ return sorted(command_handlers(command_context))
+
+
+def _get_parser_options(parser):
+ options = {}
+ for action in parser._actions:
+ # ignore positional args
+ if not action.option_strings:
+ continue
+
+ # ignore suppressed args
+ if action.help == argparse.SUPPRESS:
+ continue
+
+ options[tuple(action.option_strings)] = action.help or ""
+ return options
+
+
+@memoize
+def global_options(command_context):
+ """Return a dict of global options.
+
+ Of the form `{("-o", "--option"): "description"}`.
+ """
+ for group in command_context._mach_context.global_parser._action_groups:
+ if group.title == "Global Arguments":
+ return _get_parser_options(group)
+
+
+@memoize
+def _get_handler_options(handler):
+ """Return a dict of options for the given handler.
+
+ Of the form `{("-o", "--option"): "description"}`.
+ """
+ options = {}
+ for option_strings, val in handler.arguments:
+ # ignore positional args
+ if option_strings[0][0] != "-":
+ continue
+
+ options[tuple(option_strings)] = val.get("help", "")
+
+ if handler._parser:
+ options.update(_get_parser_options(handler.parser))
+
+ return options
+
+
+def _get_handler_info(handler):
+ try:
+ options = _get_handler_options(handler)
+ except (Exception, SystemExit):
+ # We don't want misbehaving commands to break tab completion,
+ # ignore any exceptions.
+ options = {}
+
+ subcommands = []
+ for sub in sorted(handler.subcommand_handlers):
+ subcommands.append(_get_handler_info(handler.subcommand_handlers[sub]))
+
+ return CommandInfo(
+ name=handler.name,
+ description=handler.description or "",
+ options=options,
+ subcommands=subcommands,
+ subcommand=handler.subcommand,
+ )
+
+
+@memoize
+def commands_info(command_context):
+ """Return a list of CommandInfo objects for each command."""
+ commands_info = []
+ # Loop over self.commands() rather than self.command_handlers().items() for
+ # alphabetical order.
+ for c in commands(command_context):
+ commands_info.append(_get_handler_info(command_handlers(command_context)[c]))
+ return commands_info
+
+
+@Command("mach-commands", category="misc", description="List all mach commands.")
+def run_commands(command_context):
+ print("\n".join(commands(command_context)))
+
+
+@Command(
+ "mach-debug-commands",
+ category="misc",
+ description="Show info about available mach commands.",
+)
+@CommandArgument(
+ "match",
+ metavar="MATCH",
+ default=None,
+ nargs="?",
+ help="Only display commands containing given substring.",
+)
+def run_debug_commands(command_context, match=None):
+ import inspect
+
+ for command, handler in command_handlers(command_context).items():
+ if match and match not in command:
+ continue
+
+ func = handler.func
+
+ print(command)
+ print("=" * len(command))
+ print("")
+ print("File: %s" % inspect.getsourcefile(func))
+ print("Function: %s" % func.__name__)
+ print("")
+
+
+@Command(
+ "mach-completion",
+ category="misc",
+ description="Prints a list of completion strings for the specified command.",
+)
+@CommandArgument(
+ "args", default=None, nargs=argparse.REMAINDER, help="Command to complete."
+)
+def run_completion(command_context, args):
+ if not args:
+ print("\n".join(commands(command_context)))
+ return
+
+ is_help = "help" in args
+ command = None
+ for i, arg in enumerate(args):
+ if arg in commands(command_context):
+ command = arg
+ args = args[i + 1 :]
+ break
+
+ # If no command is typed yet, just offer the commands.
+ if not command:
+ print("\n".join(commands(command_context)))
+ return
+
+ handler = command_handlers(command_context)[command]
+ # If a subcommand was typed, update the handler.
+ for arg in args:
+ if arg in handler.subcommand_handlers:
+ handler = handler.subcommand_handlers[arg]
+ break
+
+ targets = sorted(handler.subcommand_handlers.keys())
+ if is_help:
+ print("\n".join(targets))
+ return
+
+ targets.append("help")
+ targets.extend(chain(*_get_handler_options(handler).keys()))
+ print("\n".join(targets))
+
+
+def _zsh_describe(value, description=None):
+ value = '"' + value.replace(":", "\\:")
+ if description:
+ description = subprocess.list2cmdline(
+ [re.sub(r'(["\'#&;`|*?~<>^()\[\]{}$\\\x0A\xFF])', r"\\\1", description)]
+ ).lstrip('"')
+
+ if description.endswith('"') and not description.endswith(r"\""):
+ description = description[:-1]
+
+ value += ":{}".format(description)
+
+ value += '"'
+
+ return value
+
+
+@SubCommand(
+ "mach-completion",
+ "bash",
+ description="Print mach completion script for bash shell",
+)
+@CommandArgument(
+ "-f",
+ "--file",
+ dest="outfile",
+ default=None,
+ help="File path to save completion script.",
+)
+def completion_bash(command_context, outfile):
+ commands_subcommands = []
+ case_options = []
+ case_subcommands = []
+ for i, cmd in enumerate(commands_info(command_context)):
+ # Build case statement for options.
+ options = []
+ for opt_strs, description in cmd.options.items():
+ for opt in opt_strs:
+ options.append(_zsh_describe(opt, None).strip('"'))
+
+ if options:
+ case_options.append(
+ "\n".join(
+ [
+ " ({})".format(cmd.name),
+ ' opts="${{opts}} {}"'.format(" ".join(options)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ # Build case statement for subcommand options.
+ for sub in cmd.subcommands:
+ options = []
+ for opt_strs, description in sub.options.items():
+ for opt in opt_strs:
+ options.append(_zsh_describe(opt, None))
+
+ if options:
+ case_options.append(
+ "\n".join(
+ [
+ ' ("{} {}")'.format(sub.name, sub.subcommand),
+ ' opts="${{opts}} {}"'.format(" ".join(options)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ # Build case statement for subcommands.
+ subcommands = [_zsh_describe(s.subcommand, None) for s in cmd.subcommands]
+ if subcommands:
+ commands_subcommands.append(
+ '[{}]=" {} "'.format(
+ cmd.name, " ".join([h.subcommand for h in cmd.subcommands])
+ )
+ )
+
+ case_subcommands.append(
+ "\n".join(
+ [
+ " ({})".format(cmd.name),
+ ' subs="${{subs}} {}"'.format(" ".join(subcommands)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ globalopts = [
+ opt for opt_strs in global_options(command_context) for opt in opt_strs
+ ]
+ context = {
+ "case_options": "\n".join(case_options),
+ "case_subcommands": "\n".join(case_subcommands),
+ "commands": " ".join(commands(command_context)),
+ "commands_subcommands": " ".join(sorted(commands_subcommands)),
+ "globalopts": " ".join(sorted(globalopts)),
+ }
+
+ outfile = open(outfile, "w") if outfile else sys.stdout
+ print(render_template("bash", context), file=outfile)
+
+
+@SubCommand(
+ "mach-completion",
+ "zsh",
+ description="Print mach completion script for zsh shell",
+)
+@CommandArgument(
+ "-f",
+ "--file",
+ dest="outfile",
+ default=None,
+ help="File path to save completion script.",
+)
+def completion_zsh(command_context, outfile):
+ commands_descriptions = []
+ commands_subcommands = []
+ case_options = []
+ case_subcommands = []
+ for i, cmd in enumerate(commands_info(command_context)):
+ commands_descriptions.append(_zsh_describe(cmd.name, cmd.description))
+
+ # Build case statement for options.
+ options = []
+ for opt_strs, description in cmd.options.items():
+ for opt in opt_strs:
+ options.append(_zsh_describe(opt, description))
+
+ if options:
+ case_options.append(
+ "\n".join(
+ [
+ " ({})".format(cmd.name),
+ " opts+=({})".format(" ".join(options)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ # Build case statement for subcommand options.
+ for sub in cmd.subcommands:
+ options = []
+ for opt_strs, description in sub.options.items():
+ for opt in opt_strs:
+ options.append(_zsh_describe(opt, description))
+
+ if options:
+ case_options.append(
+ "\n".join(
+ [
+ " ({} {})".format(sub.name, sub.subcommand),
+ " opts+=({})".format(" ".join(options)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ # Build case statement for subcommands.
+ subcommands = [
+ _zsh_describe(s.subcommand, s.description) for s in cmd.subcommands
+ ]
+ if subcommands:
+ commands_subcommands.append(
+ '[{}]=" {} "'.format(
+ cmd.name, " ".join([h.subcommand for h in cmd.subcommands])
+ )
+ )
+
+ case_subcommands.append(
+ "\n".join(
+ [
+ " ({})".format(cmd.name),
+ " subs+=({})".format(" ".join(subcommands)),
+ " ;;",
+ "",
+ ]
+ )
+ )
+
+ globalopts = []
+ for opt_strings, description in global_options(command_context).items():
+ for opt in opt_strings:
+ globalopts.append(_zsh_describe(opt, description))
+
+ context = {
+ "case_options": "\n".join(case_options),
+ "case_subcommands": "\n".join(case_subcommands),
+ "commands": " ".join(sorted(commands_descriptions)),
+ "commands_subcommands": " ".join(sorted(commands_subcommands)),
+ "globalopts": " ".join(sorted(globalopts)),
+ }
+
+ outfile = open(outfile, "w") if outfile else sys.stdout
+ print(render_template("zsh", context), file=outfile)
+
+
+@SubCommand(
+ "mach-completion",
+ "fish",
+ description="Print mach completion script for fish shell",
+)
+@CommandArgument(
+ "-f",
+ "--file",
+ dest="outfile",
+ default=None,
+ help="File path to save completion script.",
+)
+def completion_fish(command_context, outfile):
+ def _append_opt_strs(comp, opt_strs):
+ for opt in opt_strs:
+ if opt.startswith("--"):
+ comp += " -l {}".format(opt[2:])
+ elif opt.startswith("-"):
+ comp += " -s {}".format(opt[1:])
+ return comp
+
+ globalopts = []
+ for opt_strs, description in global_options(command_context).items():
+ comp = (
+ "complete -c mach -n '__fish_mach_complete_no_command' "
+ "-d '{}'".format(description.replace("'", "\\'"))
+ )
+ comp = _append_opt_strs(comp, opt_strs)
+ globalopts.append(comp)
+
+ cmds = []
+ cmds_opts = []
+ for i, cmd in enumerate(commands_info(command_context)):
+ cmds.append(
+ "complete -c mach -f -n '__fish_mach_complete_no_command' "
+ "-a {} -d '{}'".format(cmd.name, cmd.description.replace("'", "\\'"))
+ )
+
+ cmds_opts += ["# {}".format(cmd.name)]
+
+ subcommands = " ".join([s.subcommand for s in cmd.subcommands])
+ for opt_strs, description in cmd.options.items():
+ comp = (
+ "complete -c mach -A -n '__fish_mach_complete_command {} {}' "
+ "-d '{}'".format(cmd.name, subcommands, description.replace("'", "\\'"))
+ )
+ comp = _append_opt_strs(comp, opt_strs)
+ cmds_opts.append(comp)
+
+ for sub in cmd.subcommands:
+
+ for opt_strs, description in sub.options.items():
+ comp = (
+ "complete -c mach -A -n '__fish_mach_complete_subcommand {} {}' "
+ "-d '{}'".format(
+ sub.name, sub.subcommand, description.replace("'", "\\'")
+ )
+ )
+ comp = _append_opt_strs(comp, opt_strs)
+ cmds_opts.append(comp)
+
+ description = sub.description or ""
+ description = description.replace("'", "\\'")
+ comp = (
+ "complete -c mach -A -n '__fish_mach_complete_command {} {}' "
+ "-d '{}' -a {}".format(
+ cmd.name, subcommands, description, sub.subcommand
+ )
+ )
+ cmds_opts.append(comp)
+
+ if i < len(commands(command_context)) - 1:
+ cmds_opts.append("")
+
+ context = {
+ "commands": " ".join(commands(command_context)),
+ "command_completions": "\n".join(cmds),
+ "command_option_completions": "\n".join(cmds_opts),
+ "global_option_completions": "\n".join(globalopts),
+ }
+
+ outfile = open(outfile, "w") if outfile else sys.stdout
+ print(render_template("fish", context), file=outfile)