# 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:
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)