diff options
Diffstat (limited to 'python/mach/mach/commands/commandinfo.py')
-rw-r--r-- | python/mach/mach/commands/commandinfo.py | 487 |
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) |