summaryrefslogtreecommitdiffstats
path: root/python/mach/mach/commands
diff options
context:
space:
mode:
Diffstat (limited to 'python/mach/mach/commands')
-rw-r--r--python/mach/mach/commands/__init__.py0
-rw-r--r--python/mach/mach/commands/commandinfo.py487
-rw-r--r--python/mach/mach/commands/completion_templates/bash.template62
-rw-r--r--python/mach/mach/commands/completion_templates/fish.template64
-rw-r--r--python/mach/mach/commands/completion_templates/zsh.template62
-rw-r--r--python/mach/mach/commands/settings.py51
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))