Add support for aliases (#17229)
Add a new config section: `config:aliases`, which is a dictionary mapping aliases to commands. For instance: ```yaml config: aliases: sp: spec -I ``` will define a new command `sp` that will execute `spec` with the `-I` argument. Aliases cannot override existing commands, and this is ensured with a test. We cannot currently alias subcommands. Spack will warn about any aliases containing a space, but will not error, which leaves room for subcommand aliases in the future. --------- Co-authored-by: Todd Gamblin <tgamblin@llnl.gov>
This commit is contained in:
parent
461eb944bd
commit
5074b7e922
6 changed files with 101 additions and 14 deletions
|
@ -229,3 +229,11 @@ config:
|
|||
flags:
|
||||
# Whether to keep -Werror flags active in package builds.
|
||||
keep_werror: 'none'
|
||||
|
||||
# A mapping of aliases that can be used to define new commands. For instance,
|
||||
# `sp: spec -I` will define a new command `sp` that will execute `spec` with
|
||||
# the `-I` argument. Aliases cannot override existing commands.
|
||||
aliases:
|
||||
concretise: concretize
|
||||
containerise: containerize
|
||||
rm: remove
|
||||
|
|
|
@ -304,3 +304,17 @@ To work properly, this requires your terminal to reset its title after
|
|||
Spack has finished its work, otherwise Spack's status information will
|
||||
remain in the terminal's title indefinitely. Most terminals should already
|
||||
be set up this way and clear Spack's status information.
|
||||
|
||||
-----------
|
||||
``aliases``
|
||||
-----------
|
||||
|
||||
Aliases can be used to define new Spack commands. They can be either shortcuts
|
||||
for longer commands or include specific arguments for convenience. For instance,
|
||||
if users want to use ``spack install``'s ``-v`` argument all the time, they can
|
||||
create a new alias called ``inst`` that will always call ``install -v``:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
aliases:
|
||||
inst: install -v
|
||||
|
|
|
@ -796,7 +796,9 @@ def names(args: Namespace, out: IO) -> None:
|
|||
commands = copy.copy(spack.cmd.all_commands())
|
||||
|
||||
if args.aliases:
|
||||
commands.extend(spack.main.aliases.keys())
|
||||
aliases = spack.config.get("config:aliases")
|
||||
if aliases:
|
||||
commands.extend(aliases.keys())
|
||||
|
||||
colify(commands, output=out)
|
||||
|
||||
|
@ -812,8 +814,10 @@ def bash(args: Namespace, out: IO) -> None:
|
|||
parser = spack.main.make_argument_parser()
|
||||
spack.main.add_all_commands(parser)
|
||||
|
||||
aliases = ";".join(f"{key}:{val}" for key, val in spack.main.aliases.items())
|
||||
out.write(f'SPACK_ALIASES="{aliases}"\n\n')
|
||||
aliases_config = spack.config.get("config:aliases")
|
||||
if aliases_config:
|
||||
aliases = ";".join(f"{key}:{val}" for key, val in aliases_config.items())
|
||||
out.write(f'SPACK_ALIASES="{aliases}"\n\n')
|
||||
|
||||
writer = BashCompletionWriter(parser.prog, out, args.aliases)
|
||||
writer.write(parser)
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
import os.path
|
||||
import pstats
|
||||
import re
|
||||
import shlex
|
||||
import signal
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import traceback
|
||||
import warnings
|
||||
from typing import List, Tuple
|
||||
|
||||
import archspec.cpu
|
||||
|
||||
|
@ -49,9 +51,6 @@
|
|||
#: names of profile statistics
|
||||
stat_names = pstats.Stats.sort_arg_dict_default
|
||||
|
||||
#: top-level aliases for Spack commands
|
||||
aliases = {"concretise": "concretize", "containerise": "containerize", "rm": "remove"}
|
||||
|
||||
#: help levels in order of detail (i.e., number of commands shown)
|
||||
levels = ["short", "long"]
|
||||
|
||||
|
@ -359,7 +358,10 @@ def add_command(self, cmd_name):
|
|||
module = spack.cmd.get_module(cmd_name)
|
||||
|
||||
# build a list of aliases
|
||||
alias_list = [k for k, v in aliases.items() if v == cmd_name]
|
||||
alias_list = []
|
||||
aliases = spack.config.get("config:aliases")
|
||||
if aliases:
|
||||
alias_list = [k for k, v in aliases.items() if shlex.split(v)[0] == cmd_name]
|
||||
|
||||
subparser = self.subparsers.add_parser(
|
||||
cmd_name,
|
||||
|
@ -670,7 +672,6 @@ def __init__(self, command_name, subprocess=False):
|
|||
Windows, where it is always False.
|
||||
"""
|
||||
self.parser = make_argument_parser()
|
||||
self.command = self.parser.add_command(command_name)
|
||||
self.command_name = command_name
|
||||
# TODO: figure out how to support this on windows
|
||||
self.subprocess = subprocess if sys.platform != "win32" else False
|
||||
|
@ -702,13 +703,14 @@ def __call__(self, *argv, **kwargs):
|
|||
|
||||
if self.subprocess:
|
||||
p = sp.Popen(
|
||||
[spack.paths.spack_script, self.command_name] + prepend + list(argv),
|
||||
[spack.paths.spack_script] + prepend + [self.command_name] + list(argv),
|
||||
stdout=sp.PIPE,
|
||||
stderr=sp.STDOUT,
|
||||
)
|
||||
out, self.returncode = p.communicate()
|
||||
out = out.decode()
|
||||
else:
|
||||
command = self.parser.add_command(self.command_name)
|
||||
args, unknown = self.parser.parse_known_args(
|
||||
prepend + [self.command_name] + list(argv)
|
||||
)
|
||||
|
@ -716,7 +718,7 @@ def __call__(self, *argv, **kwargs):
|
|||
out = io.StringIO()
|
||||
try:
|
||||
with log_output(out, echo=True):
|
||||
self.returncode = _invoke_command(self.command, self.parser, args, unknown)
|
||||
self.returncode = _invoke_command(command, self.parser, args, unknown)
|
||||
|
||||
except SystemExit as e:
|
||||
self.returncode = e.code
|
||||
|
@ -870,6 +872,46 @@ def restore_macos_dyld_vars():
|
|||
os.environ[dyld_var] = os.environ[stored_var_name]
|
||||
|
||||
|
||||
def resolve_alias(cmd_name: str, cmd: List[str]) -> Tuple[str, List[str]]:
|
||||
"""Resolves aliases in the given command.
|
||||
|
||||
Args:
|
||||
cmd_name: command name.
|
||||
cmd: command line arguments.
|
||||
|
||||
Returns:
|
||||
new command name and arguments.
|
||||
"""
|
||||
all_commands = spack.cmd.all_commands()
|
||||
aliases = spack.config.get("config:aliases")
|
||||
|
||||
if aliases:
|
||||
for key, value in aliases.items():
|
||||
if " " in key:
|
||||
tty.warn(
|
||||
f"Alias '{key}' (mapping to '{value}') contains a space"
|
||||
", which is not supported."
|
||||
)
|
||||
if key in all_commands:
|
||||
tty.warn(
|
||||
f"Alias '{key}' (mapping to '{value}') attempts to override"
|
||||
" built-in command."
|
||||
)
|
||||
|
||||
if cmd_name not in all_commands:
|
||||
alias = None
|
||||
|
||||
if aliases:
|
||||
alias = aliases.get(cmd_name)
|
||||
|
||||
if alias is not None:
|
||||
alias_parts = shlex.split(alias)
|
||||
cmd_name = alias_parts[0]
|
||||
cmd = alias_parts + cmd[1:]
|
||||
|
||||
return cmd_name, cmd
|
||||
|
||||
|
||||
def _main(argv=None):
|
||||
"""Logic for the main entry point for the Spack command.
|
||||
|
||||
|
@ -962,7 +1004,7 @@ def _main(argv=None):
|
|||
|
||||
# Try to load the particular command the caller asked for.
|
||||
cmd_name = args.command[0]
|
||||
cmd_name = aliases.get(cmd_name, cmd_name)
|
||||
cmd_name, args.command = resolve_alias(cmd_name, args.command)
|
||||
|
||||
# set up a bootstrap context, if asked.
|
||||
# bootstrap context needs to include parsing the command, b/c things
|
||||
|
@ -974,14 +1016,14 @@ def _main(argv=None):
|
|||
bootstrap_context = bootstrap.ensure_bootstrap_configuration()
|
||||
|
||||
with bootstrap_context:
|
||||
return finish_parse_and_run(parser, cmd_name, env_format_error)
|
||||
return finish_parse_and_run(parser, cmd_name, args.command, env_format_error)
|
||||
|
||||
|
||||
def finish_parse_and_run(parser, cmd_name, env_format_error):
|
||||
def finish_parse_and_run(parser, cmd_name, cmd, env_format_error):
|
||||
"""Finish parsing after we know the command to run."""
|
||||
# add the found command to the parser and re-run then re-parse
|
||||
command = parser.add_command(cmd_name)
|
||||
args, unknown = parser.parse_known_args()
|
||||
args, unknown = parser.parse_known_args(cmd)
|
||||
|
||||
# Now that we know what command this is and what its args are, determine
|
||||
# whether we can continue with a bad environment and raise if not.
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"url_fetch_method": {"type": "string", "enum": ["urllib", "curl"]},
|
||||
"additional_external_search_paths": {"type": "array", "items": {"type": "string"}},
|
||||
"binary_index_ttl": {"type": "integer", "minimum": 0},
|
||||
"aliases": {"type": "object", "patternProperties": {r"\w[\w-]*": {"type": "string"}}},
|
||||
},
|
||||
"deprecatedProperties": {
|
||||
"properties": ["terminal_title"],
|
||||
|
|
|
@ -58,6 +58,24 @@ def test_subcommands():
|
|||
assert "spack compiler add" in out2
|
||||
|
||||
|
||||
@pytest.mark.not_on_windows("subprocess not supported on Windows")
|
||||
def test_override_alias():
|
||||
"""Test that spack commands cannot be overriden by aliases."""
|
||||
|
||||
install = spack.main.SpackCommand("install", subprocess=True)
|
||||
instal = spack.main.SpackCommand("instal", subprocess=True)
|
||||
|
||||
out = install(fail_on_error=False, global_args=["-c", "config:aliases:install:find"])
|
||||
assert "install requires a package argument or active environment" in out
|
||||
assert "Alias 'install' (mapping to 'find') attempts to override built-in command" in out
|
||||
|
||||
out = install(fail_on_error=False, global_args=["-c", "config:aliases:foo bar:find"])
|
||||
assert "Alias 'foo bar' (mapping to 'find') contains a space, which is not supported" in out
|
||||
|
||||
out = instal(fail_on_error=False, global_args=["-c", "config:aliases:instal:find"])
|
||||
assert "install requires a package argument or active environment" not in out
|
||||
|
||||
|
||||
def test_rst():
|
||||
"""Do some simple sanity checks of the rst writer."""
|
||||
out1 = commands("--format=rst")
|
||||
|
|
Loading…
Reference in a new issue