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
@ -229,3 +229,11 @@ config:
|
|||||||
flags:
|
flags:
|
||||||
# Whether to keep -Werror flags active in package builds.
|
# Whether to keep -Werror flags active in package builds.
|
||||||
keep_werror: 'none'
|
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
|
Spack has finished its work, otherwise Spack's status information will
|
||||||
remain in the terminal's title indefinitely. Most terminals should already
|
remain in the terminal's title indefinitely. Most terminals should already
|
||||||
be set up this way and clear Spack's status information.
|
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())
|
commands = copy.copy(spack.cmd.all_commands())
|
||||||
|
|
||||||
if args.aliases:
|
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)
|
colify(commands, output=out)
|
||||||
|
|
||||||
@ -812,8 +814,10 @@ def bash(args: Namespace, out: IO) -> None:
|
|||||||
parser = spack.main.make_argument_parser()
|
parser = spack.main.make_argument_parser()
|
||||||
spack.main.add_all_commands(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")
|
||||||
out.write(f'SPACK_ALIASES="{aliases}"\n\n')
|
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 = BashCompletionWriter(parser.prog, out, args.aliases)
|
||||||
writer.write(parser)
|
writer.write(parser)
|
||||||
|
@ -16,11 +16,13 @@
|
|||||||
import os.path
|
import os.path
|
||||||
import pstats
|
import pstats
|
||||||
import re
|
import re
|
||||||
|
import shlex
|
||||||
import signal
|
import signal
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
import archspec.cpu
|
import archspec.cpu
|
||||||
|
|
||||||
@ -49,9 +51,6 @@
|
|||||||
#: names of profile statistics
|
#: names of profile statistics
|
||||||
stat_names = pstats.Stats.sort_arg_dict_default
|
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)
|
#: help levels in order of detail (i.e., number of commands shown)
|
||||||
levels = ["short", "long"]
|
levels = ["short", "long"]
|
||||||
|
|
||||||
@ -359,7 +358,10 @@ def add_command(self, cmd_name):
|
|||||||
module = spack.cmd.get_module(cmd_name)
|
module = spack.cmd.get_module(cmd_name)
|
||||||
|
|
||||||
# build a list of aliases
|
# 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(
|
subparser = self.subparsers.add_parser(
|
||||||
cmd_name,
|
cmd_name,
|
||||||
@ -670,7 +672,6 @@ def __init__(self, command_name, subprocess=False):
|
|||||||
Windows, where it is always False.
|
Windows, where it is always False.
|
||||||
"""
|
"""
|
||||||
self.parser = make_argument_parser()
|
self.parser = make_argument_parser()
|
||||||
self.command = self.parser.add_command(command_name)
|
|
||||||
self.command_name = command_name
|
self.command_name = command_name
|
||||||
# TODO: figure out how to support this on windows
|
# TODO: figure out how to support this on windows
|
||||||
self.subprocess = subprocess if sys.platform != "win32" else False
|
self.subprocess = subprocess if sys.platform != "win32" else False
|
||||||
@ -702,13 +703,14 @@ def __call__(self, *argv, **kwargs):
|
|||||||
|
|
||||||
if self.subprocess:
|
if self.subprocess:
|
||||||
p = sp.Popen(
|
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,
|
stdout=sp.PIPE,
|
||||||
stderr=sp.STDOUT,
|
stderr=sp.STDOUT,
|
||||||
)
|
)
|
||||||
out, self.returncode = p.communicate()
|
out, self.returncode = p.communicate()
|
||||||
out = out.decode()
|
out = out.decode()
|
||||||
else:
|
else:
|
||||||
|
command = self.parser.add_command(self.command_name)
|
||||||
args, unknown = self.parser.parse_known_args(
|
args, unknown = self.parser.parse_known_args(
|
||||||
prepend + [self.command_name] + list(argv)
|
prepend + [self.command_name] + list(argv)
|
||||||
)
|
)
|
||||||
@ -716,7 +718,7 @@ def __call__(self, *argv, **kwargs):
|
|||||||
out = io.StringIO()
|
out = io.StringIO()
|
||||||
try:
|
try:
|
||||||
with log_output(out, echo=True):
|
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:
|
except SystemExit as e:
|
||||||
self.returncode = e.code
|
self.returncode = e.code
|
||||||
@ -870,6 +872,46 @@ def restore_macos_dyld_vars():
|
|||||||
os.environ[dyld_var] = os.environ[stored_var_name]
|
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):
|
def _main(argv=None):
|
||||||
"""Logic for the main entry point for the Spack command.
|
"""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.
|
# Try to load the particular command the caller asked for.
|
||||||
cmd_name = args.command[0]
|
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.
|
# set up a bootstrap context, if asked.
|
||||||
# bootstrap context needs to include parsing the command, b/c things
|
# 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()
|
bootstrap_context = bootstrap.ensure_bootstrap_configuration()
|
||||||
|
|
||||||
with bootstrap_context:
|
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."""
|
"""Finish parsing after we know the command to run."""
|
||||||
# add the found command to the parser and re-run then re-parse
|
# add the found command to the parser and re-run then re-parse
|
||||||
command = parser.add_command(cmd_name)
|
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
|
# 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.
|
# whether we can continue with a bad environment and raise if not.
|
||||||
|
@ -92,6 +92,7 @@
|
|||||||
"url_fetch_method": {"type": "string", "enum": ["urllib", "curl"]},
|
"url_fetch_method": {"type": "string", "enum": ["urllib", "curl"]},
|
||||||
"additional_external_search_paths": {"type": "array", "items": {"type": "string"}},
|
"additional_external_search_paths": {"type": "array", "items": {"type": "string"}},
|
||||||
"binary_index_ttl": {"type": "integer", "minimum": 0},
|
"binary_index_ttl": {"type": "integer", "minimum": 0},
|
||||||
|
"aliases": {"type": "object", "patternProperties": {r"\w[\w-]*": {"type": "string"}}},
|
||||||
},
|
},
|
||||||
"deprecatedProperties": {
|
"deprecatedProperties": {
|
||||||
"properties": ["terminal_title"],
|
"properties": ["terminal_title"],
|
||||||
|
@ -58,6 +58,24 @@ def test_subcommands():
|
|||||||
assert "spack compiler add" in out2
|
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():
|
def test_rst():
|
||||||
"""Do some simple sanity checks of the rst writer."""
|
"""Do some simple sanity checks of the rst writer."""
|
||||||
out1 = commands("--format=rst")
|
out1 = commands("--format=rst")
|
||||||
|
Loading…
Reference in New Issue
Block a user