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:
Michael Kuhn 2023-11-06 23:37:46 +01:00 committed by GitHub
parent 461eb944bd
commit 5074b7e922
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 101 additions and 14 deletions

View file

@ -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

View file

@ -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

View file

@ -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,7 +814,9 @@ 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())
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)

View file

@ -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.

View file

@ -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"],

View file

@ -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")