diff options
Diffstat (limited to 'python/mach/mach/commands')
-rw-r--r-- | python/mach/mach/commands/__init__.py | 0 | ||||
-rw-r--r-- | python/mach/mach/commands/commandinfo.py | 487 | ||||
-rw-r--r-- | python/mach/mach/commands/completion_templates/bash.template | 62 | ||||
-rw-r--r-- | python/mach/mach/commands/completion_templates/fish.template | 64 | ||||
-rw-r--r-- | python/mach/mach/commands/completion_templates/zsh.template | 62 | ||||
-rw-r--r-- | python/mach/mach/commands/settings.py | 51 |
6 files changed, 726 insertions, 0 deletions
diff --git a/python/mach/mach/commands/__init__.py b/python/mach/mach/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/python/mach/mach/commands/__init__.py 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) diff --git a/python/mach/mach/commands/completion_templates/bash.template b/python/mach/mach/commands/completion_templates/bash.template new file mode 100644 index 0000000000..5372308702 --- /dev/null +++ b/python/mach/mach/commands/completion_templates/bash.template @@ -0,0 +1,62 @@ +_mach_complete() +{ + local com coms comsubs cur opts script sub subs + COMPREPLY=() + declare -A comsubs=( %(commands_subcommands)s ) + + _get_comp_words_by_ref -n : cur words + # for an alias, get the real script behind it + if [[ $(type -t ${words[0]}) == "alias" ]]; then + script=$(alias ${words[0]} | sed -E "s/alias ${words[0]}='(.*)'/\\1/") + else + script=${words[0]} + fi + # lookup for command and subcommand + for word in ${words[@]:1}; do + if [[ $word == -* ]]; then + continue + fi + + if [[ -z $com ]]; then + com=$word + elif [[ "${comsubs[$com]}" == *" $word "* ]]; then + sub=$word + break + fi + done + # completing for an option + if [[ ${cur} == -* ]] ; then + if [[ -n $com ]]; then + if [[ -n $sub ]]; then + optkey="$com $sub" + else + optkey="$com" + fi + case $optkey in +%(case_options)s + esac + else + # no command, complete global options + opts="%(globalopts)s" + fi + COMPREPLY=($(compgen -W "${opts}" -- ${cur})) + __ltrim_colon_completions "$cur" + return 0; + # completing for a command + elif [[ $cur == $com ]]; then + coms="%(commands)s" + COMPREPLY=($(compgen -W "${coms}" -- ${cur})) + __ltrim_colon_completions "$cur" + return 0 + else + if [[ -z $sub ]]; then + case "$com" in +%(case_subcommands)s + esac + COMPREPLY=($(compgen -W "${subs}" -- ${cur})) + __ltrim_colon_completions "$cur" + fi + return 0 + fi +} +complete -o default -F _mach_complete mach diff --git a/python/mach/mach/commands/completion_templates/fish.template b/python/mach/mach/commands/completion_templates/fish.template new file mode 100644 index 0000000000..8373ee4080 --- /dev/null +++ b/python/mach/mach/commands/completion_templates/fish.template @@ -0,0 +1,64 @@ +function __fish_mach_complete_no_command + for i in (commandline -opc) + if contains -- $i %(commands)s + return 1 + end + end + return 0 +end + +function __fish_mach_complete_command_matches + for i in (commandline -opc) + if contains -- $i %(commands)s + set com $i + break + end + end + + if not set -q com + return 1 + end + + if test "$com" != "$argv" + return 1 + end + return 0 +end + +function __fish_mach_complete_command + __fish_mach_complete_command_matches $argv[1] + if test $status -ne 0 + return 1 + end + + # If a subcommand is already entered, don't complete, we should defer to + # '__fish_mach_complete_subcommand'. + for i in (commandline -opc) + if contains -- $i $argv[2..-1] + return 1 + end + end + return 0 +end + +function __fish_mach_complete_subcommand + __fish_mach_complete_command_matches $argv[1] + if test $status -ne 0 + return 1 + end + + # Command matches, now check for subcommand + for i in (commandline -opc) + if contains -- $i $argv[2] + return 0 + end + end + return 1 +end + +# global options +%(global_option_completions)s +# commands +%(command_completions)s +# command options +%(command_option_completions)s diff --git a/python/mach/mach/commands/completion_templates/zsh.template b/python/mach/mach/commands/completion_templates/zsh.template new file mode 100644 index 0000000000..21677841ef --- /dev/null +++ b/python/mach/mach/commands/completion_templates/zsh.template @@ -0,0 +1,62 @@ +#compdef mach +_mach_complete() +{ + local com coms comsubs cur optkey opts state sub subs + cur=${words[${#words[@]}]} + typeset -A comsubs + comsubs=( %(commands_subcommands)s ) + + # lookup for command and subcommand + for word in ${words[@]:1}; do + if [[ $word == -* ]]; then + continue + fi + + if [[ -z $com ]]; then + com=$word + elif [[ ${comsubs[$com]} == *" $word "* ]]; then + sub=$word + break + fi + done + + # check for a subcommand + if [[ $cur == $com ]]; then + state="command" + coms=(%(commands)s) + elif [[ ${cur} == -* ]]; then + state="option" + if [[ -z $com ]]; then + # no command, use global options + opts=(%(globalopts)s) + fi + fi + case $state in + (command) + _describe 'command' coms + ;; + (option) + if [[ -n $sub ]]; then + optkey="$com $sub" + else + optkey="$com" + fi + case $optkey in +%(case_options)s + esac + _describe 'option' opts + ;; + *) + if [[ -z $sub ]]; then + # if we're completing a command with subcommands, add them here + case "$com" in +%(case_subcommands)s + esac + _describe 'subcommand' subs + fi + # also fallback to file completion + _arguments '*:file:_files' + esac +} +_mach_complete "$@" +compdef _mach_complete mach diff --git a/python/mach/mach/commands/settings.py b/python/mach/mach/commands/settings.py new file mode 100644 index 0000000000..8e168a3921 --- /dev/null +++ b/python/mach/mach/commands/settings.py @@ -0,0 +1,51 @@ +# 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/. + +from textwrap import TextWrapper + +from mach.config import TYPE_CLASSES +from mach.decorators import Command, CommandArgument + + +# Interact with settings for mach. + +# Currently, we only provide functionality to view what settings are +# available. In the future, this module will be used to modify settings, help +# people create configs via a wizard, etc. + + +@Command("settings", category="devenv", description="Show available config settings.") +@CommandArgument( + "-l", + "--list", + dest="short", + action="store_true", + help="Show settings in a concise list", +) +def run_settings(command_context, short=None): + """List available settings.""" + types = {v: k for k, v in TYPE_CLASSES.items()} + wrapper = TextWrapper(initial_indent="# ", subsequent_indent="# ") + for i, section in enumerate(sorted(command_context._mach_context.settings)): + if not short: + print("%s[%s]" % ("" if i == 0 else "\n", section)) + + for option in sorted(command_context._mach_context.settings[section]._settings): + meta = command_context._mach_context.settings[section].get_meta(option) + desc = meta["description"] + + if short: + print("%s.%s -- %s" % (section, option, desc.splitlines()[0])) + continue + + if option == "*": + option = "<option>" + + if "choices" in meta: + value = "{%s}" % ", ".join(meta["choices"]) + else: + value = "<%s>" % types[meta["type_cls"]] + + print(wrapper.fill(desc)) + print(";%s=%s" % (option, value)) |