summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Kuhn <michael.kuhn@ovgu.de>2023-11-06 23:37:46 +0100
committerGitHub <noreply@github.com>2023-11-06 14:37:46 -0800
commit5074b7e922fed8276367755832e3263885c8e884 (patch)
treedc6579374b5f2cbb1bcc2dfd39675bc01267ae62
parent461eb944bdff103b8e347c272afb2bcbd31f9723 (diff)
downloadspack-5074b7e922fed8276367755832e3263885c8e884.tar.gz
spack-5074b7e922fed8276367755832e3263885c8e884.tar.bz2
spack-5074b7e922fed8276367755832e3263885c8e884.tar.xz
spack-5074b7e922fed8276367755832e3263885c8e884.zip
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>
-rw-r--r--etc/spack/defaults/config.yaml8
-rw-r--r--lib/spack/docs/config_yaml.rst14
-rw-r--r--lib/spack/spack/cmd/commands.py10
-rw-r--r--lib/spack/spack/main.py64
-rw-r--r--lib/spack/spack/schema/config.py1
-rw-r--r--lib/spack/spack/test/cmd/commands.py18
6 files changed, 101 insertions, 14 deletions
diff --git a/etc/spack/defaults/config.yaml b/etc/spack/defaults/config.yaml
index b4d81f69da..018e8deb55 100644
--- a/etc/spack/defaults/config.yaml
+++ b/etc/spack/defaults/config.yaml
@@ -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
diff --git a/lib/spack/docs/config_yaml.rst b/lib/spack/docs/config_yaml.rst
index 294f7c3436..d54977beba 100644
--- a/lib/spack/docs/config_yaml.rst
+++ b/lib/spack/docs/config_yaml.rst
@@ -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
diff --git a/lib/spack/spack/cmd/commands.py b/lib/spack/spack/cmd/commands.py
index 9ebaa62239..25e1a24d00 100644
--- a/lib/spack/spack/cmd/commands.py
+++ b/lib/spack/spack/cmd/commands.py
@@ -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)
diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py
index bc29b6f1f1..87408d363a 100644
--- a/lib/spack/spack/main.py
+++ b/lib/spack/spack/main.py
@@ -16,11 +16,13 @@ import os
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 @@ from spack.error import SpackError
#: 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 @@ class SpackArgumentParser(argparse.ArgumentParser):
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 @@ class SpackCommand:
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 @@ class SpackCommand:
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 @@ class SpackCommand:
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.
diff --git a/lib/spack/spack/schema/config.py b/lib/spack/spack/schema/config.py
index 6c30f0aab9..6818cd78f3 100644
--- a/lib/spack/spack/schema/config.py
+++ b/lib/spack/spack/schema/config.py
@@ -92,6 +92,7 @@ properties = {
"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"],
diff --git a/lib/spack/spack/test/cmd/commands.py b/lib/spack/spack/test/cmd/commands.py
index 99faac72b9..3288b092d4 100644
--- a/lib/spack/spack/test/cmd/commands.py
+++ b/lib/spack/spack/test/cmd/commands.py
@@ -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")