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:
		@@ -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")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user