Implement fish completion (#29549)

* commands: provide more information to Command

* fish: Add script to generate fish completion

* fish: auto prepend `spack` command to avoid duplication

* fish: impove completion generation code readability

* commands: replace match-case with if-else

* fish: fix optspec variable name prefix

* fish: fix return value in get_optspecs

* fish: fix return value in get_optspecs

* format: split long line and trim trailing space

* bugfix: replace f-string with interpolation

* fish: compete more specs and some fixes

* fish: complete hash spec starts with /

* fish: improve compatibility

* style: trim trailing whitespace

* commands: add fish to update args and update tests

* commands: add fish completion file

* style: merge imports

* fish: source completion in setup-env

* fish: caret only completes dependencies

* fish: make sure we always get same order of output

* fish: spack activate
only show installed packages that have extensions

* fish: update completion file

* fish: make dict keys sorted

* Blacken code

* Fix bad merge

* Undo style changes to setup-env.fish

* Fix unit tests

* Style fix

* Compatible with fish_indent

* Use list for stability of order

* Sort one more place

* Sort more things

* Sorting unneeded

* Unsort

* Print difference

* Style fix

* Help messages need quotes

* Arguments to -a must be quoted

* Update types

* Update types

* Update types

* Add type hints

* Change order of positionals

* Always expand help

* Remove shared base class

* Fix type hints

* Remove platform-specific choices

* First line of help only

* Remove unused maps

* Remove suppress

* Remove debugging comments

* Better quoting

* Fish completions have no double dash

* Remove test for deleted class

* Fix grammar in header file

* Use single quotes in most places

* Better support for remainder nargs

* No magic strings

* * and + can also complete multiple

* lower case, no period

---------

Co-authored-by: Adam J. Stewart <ajstewart426@gmail.com>
This commit is contained in:
百地 希留耶 2023-07-22 21:55:12 +08:00 committed by GitHub
parent 66e85ae39a
commit 90ac0ef66e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 3947 additions and 165 deletions

View file

@ -9,7 +9,7 @@
import re
import sys
from argparse import ArgumentParser
from typing import IO, Optional, Sequence, Tuple
from typing import IO, Any, Iterable, List, Optional, Sequence, Tuple, Union
class Command:
@ -25,9 +25,9 @@ def __init__(
prog: str,
description: Optional[str],
usage: str,
positionals: Sequence[Tuple[str, str]],
optionals: Sequence[Tuple[Sequence[str], str, str]],
subcommands: Sequence[Tuple[ArgumentParser, str]],
positionals: List[Tuple[str, Optional[Iterable[Any]], Union[int, str, None], str]],
optionals: List[Tuple[Sequence[str], List[str], str, Union[int, str, None], str]],
subcommands: List[Tuple[ArgumentParser, str, str]],
) -> None:
"""Initialize a new Command instance.
@ -96,13 +96,30 @@ def parse(self, parser: ArgumentParser, prog: str) -> Command:
if action.option_strings:
flags = action.option_strings
dest_flags = fmt._format_action_invocation(action)
help = self._expand_help(action) if action.help else ""
help = help.replace("\n", " ")
optionals.append((flags, dest_flags, help))
nargs = action.nargs
help = (
self._expand_help(action)
if action.help and action.help != argparse.SUPPRESS
else ""
)
help = help.split("\n")[0]
if action.choices is not None:
dest = [str(choice) for choice in action.choices]
else:
dest = [action.dest]
optionals.append((flags, dest, dest_flags, nargs, help))
elif isinstance(action, argparse._SubParsersAction):
for subaction in action._choices_actions:
subparser = action._name_parser_map[subaction.dest]
subcommands.append((subparser, subaction.dest))
help = (
self._expand_help(subaction)
if subaction.help and action.help != argparse.SUPPRESS
else ""
)
help = help.split("\n")[0]
subcommands.append((subparser, subaction.dest, help))
# Look for aliases of the form 'name (alias, ...)'
if self.aliases and isinstance(subaction.metavar, str):
@ -111,12 +128,22 @@ def parse(self, parser: ArgumentParser, prog: str) -> Command:
aliases = match.group(2).split(", ")
for alias in aliases:
subparser = action._name_parser_map[alias]
subcommands.append((subparser, alias))
help = (
self._expand_help(subaction)
if subaction.help and action.help != argparse.SUPPRESS
else ""
)
help = help.split("\n")[0]
subcommands.append((subparser, alias, help))
else:
args = fmt._format_action_invocation(action)
help = self._expand_help(action) if action.help else ""
help = help.replace("\n", " ")
positionals.append((args, help))
help = (
self._expand_help(action)
if action.help and action.help != argparse.SUPPRESS
else ""
)
help = help.split("\n")[0]
positionals.append((args, action.choices, action.nargs, help))
return Command(prog, description, usage, positionals, optionals, subcommands)
@ -146,7 +173,7 @@ def _write(self, parser: ArgumentParser, prog: str, level: int = 0) -> None:
cmd = self.parse(parser, prog)
self.out.write(self.format(cmd))
for subparser, prog in cmd.subcommands:
for subparser, prog, help in cmd.subcommands:
self._write(subparser, prog, level=level + 1)
def write(self, parser: ArgumentParser) -> None:
@ -205,13 +232,13 @@ def format(self, cmd: Command) -> str:
if cmd.positionals:
string.write(self.begin_positionals())
for args, help in cmd.positionals:
for args, choices, nargs, help in cmd.positionals:
string.write(self.positional(args, help))
string.write(self.end_positionals())
if cmd.optionals:
string.write(self.begin_optionals())
for flags, dest_flags, help in cmd.optionals:
for flags, dest, dest_flags, nargs, help in cmd.optionals:
string.write(self.optional(dest_flags, help))
string.write(self.end_optionals())
@ -338,7 +365,7 @@ def end_optionals(self) -> str:
"""
return ""
def begin_subcommands(self, subcommands: Sequence[Tuple[ArgumentParser, str]]) -> str:
def begin_subcommands(self, subcommands: List[Tuple[ArgumentParser, str, str]]) -> str:
"""Table with links to other subcommands.
Arguments:
@ -355,114 +382,8 @@ def begin_subcommands(self, subcommands: Sequence[Tuple[ArgumentParser, str]]) -
"""
for cmd, _ in subcommands:
for cmd, _, _ in subcommands:
prog = re.sub(r"^[^ ]* ", "", cmd.prog)
string += " * :ref:`{0} <{1}>`\n".format(prog, cmd.prog.replace(" ", "-"))
return string + "\n"
class ArgparseCompletionWriter(ArgparseWriter):
"""Write argparse output as shell programmable tab completion functions."""
def format(self, cmd: Command) -> str:
"""Return the string representation of a single node in the parser tree.
Args:
cmd: Parsed information about a command or subcommand.
Returns:
String representation of this subcommand.
"""
assert cmd.optionals # we should always at least have -h, --help
assert not (cmd.positionals and cmd.subcommands) # one or the other
# We only care about the arguments/flags, not the help messages
positionals: Tuple[str, ...] = ()
if cmd.positionals:
positionals, _ = zip(*cmd.positionals)
optionals, _, _ = zip(*cmd.optionals)
subcommands: Tuple[str, ...] = ()
if cmd.subcommands:
_, subcommands = zip(*cmd.subcommands)
# Flatten lists of lists
optionals = [x for xx in optionals for x in xx]
return (
self.start_function(cmd.prog)
+ self.body(positionals, optionals, subcommands)
+ self.end_function(cmd.prog)
)
def start_function(self, prog: str) -> str:
"""Return the syntax needed to begin a function definition.
Args:
prog: Program name.
Returns:
Function definition beginning.
"""
name = prog.replace("-", "_").replace(" ", "_")
return "\n_{0}() {{".format(name)
def end_function(self, prog: str) -> str:
"""Return the syntax needed to end a function definition.
Args:
prog: Program name
Returns:
Function definition ending.
"""
return "}\n"
def body(
self, positionals: Sequence[str], optionals: Sequence[str], subcommands: Sequence[str]
) -> str:
"""Return the body of the function.
Args:
positionals: List of positional arguments.
optionals: List of optional arguments.
subcommands: List of subcommand parsers.
Returns:
Function body.
"""
return ""
def positionals(self, positionals: Sequence[str]) -> str:
"""Return the syntax for reporting positional arguments.
Args:
positionals: List of positional arguments.
Returns:
Syntax for positional arguments.
"""
return ""
def optionals(self, optionals: Sequence[str]) -> str:
"""Return the syntax for reporting optional flags.
Args:
optionals: List of optional arguments.
Returns:
Syntax for optional flags.
"""
return ""
def subcommands(self, subcommands: Sequence[str]) -> str:
"""Return the syntax for reporting subcommands.
Args:
subcommands: List of subcommand parsers.
Returns:
Syntax for subcommand parsers
"""
return ""

View file

@ -9,16 +9,11 @@
import re
import sys
from argparse import ArgumentParser, Namespace
from typing import IO, Any, Callable, Dict, Sequence, Set
from typing import IO, Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
import llnl.util.filesystem as fs
import llnl.util.tty as tty
from llnl.util.argparsewriter import (
ArgparseCompletionWriter,
ArgparseRstWriter,
ArgparseWriter,
Command,
)
from llnl.util.argparsewriter import ArgparseRstWriter, ArgparseWriter, Command
from llnl.util.tty.colify import colify
import spack.cmd
@ -43,7 +38,13 @@
"format": "bash",
"header": os.path.join(spack.paths.share_path, "bash", "spack-completion.in"),
"update": os.path.join(spack.paths.share_path, "spack-completion.bash"),
}
},
"fish": {
"aliases": True,
"format": "fish",
"header": os.path.join(spack.paths.share_path, "fish", "spack-completion.in"),
"update": os.path.join(spack.paths.share_path, "spack-completion.fish"),
},
}
@ -178,9 +179,63 @@ def format(self, cmd: Command) -> str:
}
class BashCompletionWriter(ArgparseCompletionWriter):
class BashCompletionWriter(ArgparseWriter):
"""Write argparse output as bash programmable tab completion."""
def format(self, cmd: Command) -> str:
"""Return the string representation of a single node in the parser tree.
Args:
cmd: Parsed information about a command or subcommand.
Returns:
String representation of this subcommand.
"""
assert cmd.optionals # we should always at least have -h, --help
assert not (cmd.positionals and cmd.subcommands) # one or the other
# We only care about the arguments/flags, not the help messages
positionals: Tuple[str, ...] = ()
if cmd.positionals:
positionals, _, _, _ = zip(*cmd.positionals)
optionals, _, _, _, _ = zip(*cmd.optionals)
subcommands: Tuple[str, ...] = ()
if cmd.subcommands:
_, subcommands, _ = zip(*cmd.subcommands)
# Flatten lists of lists
optionals = [x for xx in optionals for x in xx]
return (
self.start_function(cmd.prog)
+ self.body(positionals, optionals, subcommands)
+ self.end_function(cmd.prog)
)
def start_function(self, prog: str) -> str:
"""Return the syntax needed to begin a function definition.
Args:
prog: Program name.
Returns:
Function definition beginning.
"""
name = prog.replace("-", "_").replace(" ", "_")
return "\n_{0}() {{".format(name)
def end_function(self, prog: str) -> str:
"""Return the syntax needed to end a function definition.
Args:
prog: Program name
Returns:
Function definition ending.
"""
return "}\n"
def body(
self, positionals: Sequence[str], optionals: Sequence[str], subcommands: Sequence[str]
) -> str:
@ -264,6 +319,396 @@ def subcommands(self, subcommands: Sequence[str]) -> str:
return 'SPACK_COMPREPLY="{0}"'.format(" ".join(subcommands))
# Map argument destination names to their complete commands
# Earlier items in the list have higher precedence
_dest_to_fish_complete = {
("activate", "view"): "-f -a '(__fish_complete_directories)'",
("bootstrap root", "path"): "-f -a '(__fish_complete_directories)'",
("mirror add", "mirror"): "-f",
("repo add", "path"): "-f -a '(__fish_complete_directories)'",
("test find", "filter"): "-f -a '(__fish_spack_tests)'",
("bootstrap", "name"): "-f -a '(__fish_spack_bootstrap_names)'",
("buildcache create", "key"): "-f -a '(__fish_spack_gpg_keys)'",
("build-env", r"spec \[--\].*"): "-f -a '(__fish_spack_build_env_spec)'",
("checksum", "package"): "-f -a '(__fish_spack_packages)'",
(
"checksum",
"versions",
): "-f -a '(__fish_spack_package_versions $__fish_spack_argparse_argv[1])'",
("config", "path"): "-f -a '(__fish_spack_colon_path)'",
("config", "section"): "-f -a '(__fish_spack_config_sections)'",
("develop", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("diff", "specs?"): "-f -a '(__fish_spack_installed_specs)'",
("gpg sign", "output"): "-f -a '(__fish_complete_directories)'",
("gpg", "keys?"): "-f -a '(__fish_spack_gpg_keys)'",
("graph", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("help", "help_command"): "-f -a '(__fish_spack_commands)'",
("list", "filter"): "-f -a '(__fish_spack_packages)'",
("mirror", "mirror"): "-f -a '(__fish_spack_mirrors)'",
("pkg", "package"): "-f -a '(__fish_spack_pkg_packages)'",
("remove", "specs?"): "-f -a '(__fish_spack_installed_specs)'",
("repo", "namespace_or_path"): "$__fish_spack_force_files -a '(__fish_spack_repos)'",
("restage", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("rm", "specs?"): "-f -a '(__fish_spack_installed_specs)'",
("solve", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("spec", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("stage", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("test-env", r"spec \[--\].*"): "-f -a '(__fish_spack_build_env_spec)'",
("test", r"\[?name.*"): "-f -a '(__fish_spack_tests)'",
("undevelop", "specs?"): "-f -k -a '(__fish_spack_specs_or_id)'",
("verify", "specs_or_files"): "$__fish_spack_force_files -a '(__fish_spack_installed_specs)'",
("view", "path"): "-f -a '(__fish_complete_directories)'",
("", "comment"): "-f",
("", "compiler_spec"): "-f -a '(__fish_spack_installed_compilers)'",
("", "config_scopes"): "-f -a '(__fish_complete_directories)'",
("", "extendable"): "-f -a '(__fish_spack_extensions)'",
("", "installed_specs?"): "-f -a '(__fish_spack_installed_specs)'",
("", "job_url"): "-f",
("", "location_env"): "-f -a '(__fish_complete_directories)'",
("", "pytest_args"): "-f -a '(__fish_spack_unit_tests)'",
("", "package_or_file"): "$__fish_spack_force_files -a '(__fish_spack_packages)'",
("", "package_or_user"): "-f -a '(__fish_spack_packages)'",
("", "package"): "-f -a '(__fish_spack_packages)'",
("", "PKG"): "-f -a '(__fish_spack_packages)'",
("", "prefix"): "-f -a '(__fish_complete_directories)'",
("", r"rev\d?"): "-f -a '(__fish_spack_git_rev)'",
("", "specs?"): "-f -k -a '(__fish_spack_specs)'",
("", "tags?"): "-f -a '(__fish_spack_tags)'",
("", "virtual_package"): "-f -a '(__fish_spack_providers)'",
("", "working_dir"): "-f -a '(__fish_complete_directories)'",
("", r"(\w*_)?env"): "-f -a '(__fish_spack_environments)'",
("", r"(\w*_)?dir(ectory)?"): "-f -a '(__fish_spack_environments)'",
("", r"(\w*_)?mirror_name"): "-f -a '(__fish_spack_mirrors)'",
}
def _fish_dest_get_complete(prog: str, dest: str) -> Optional[str]:
"""Map from subcommand to autocompletion argument.
Args:
prog: Program name.
dest: Destination.
Returns:
Autocompletion argument.
"""
s = prog.split(None, 1)
subcmd = s[1] if len(s) == 2 else ""
for (prog_key, pos_key), value in _dest_to_fish_complete.items():
if subcmd.startswith(prog_key) and re.match("^" + pos_key + "$", dest):
return value
return None
class FishCompletionWriter(ArgparseWriter):
"""Write argparse output as bash programmable tab completion."""
def format(self, cmd: Command) -> str:
"""Return the string representation of a single node in the parser tree.
Args:
cmd: Parsed information about a command or subcommand.
Returns:
String representation of a node.
"""
assert cmd.optionals # we should always at least have -h, --help
assert not (cmd.positionals and cmd.subcommands) # one or the other
# We also need help messages and how arguments are used
# So we pass everything to completion writer
positionals = cmd.positionals
optionals = cmd.optionals
subcommands = cmd.subcommands
return (
self.prog_comment(cmd.prog)
+ self.optspecs(cmd.prog, optionals)
+ self.complete(cmd.prog, positionals, optionals, subcommands)
)
def _quote(self, string: str) -> str:
"""Quote string and escape special characters if necessary.
Args:
string: Input string.
Returns:
Quoted string.
"""
# Goal here is to match fish_indent behavior
# Strings without spaces (or other special characters) do not need to be escaped
if not any([sub in string for sub in [" ", "'", '"']]):
return string
string = string.replace("'", r"\'")
return f"'{string}'"
def optspecs(
self,
prog: str,
optionals: List[Tuple[Sequence[str], List[str], str, Union[int, str, None], str]],
) -> str:
"""Read the optionals and return the command to set optspec.
Args:
prog: Program name.
optionals: List of optional arguments.
Returns:
Command to set optspec variable.
"""
# Variables of optspecs
optspec_var = "__fish_spack_optspecs_" + prog.replace(" ", "_").replace("-", "_")
if optionals is None:
return "set -g %s\n" % optspec_var
# Build optspec by iterating over options
args = []
for flags, dest, _, nargs, _ in optionals:
if len(flags) == 0:
continue
required = ""
# Because nargs '?' is treated differently in fish, we treat it as required.
# Because multi-argument options are not supported, we treat it like one argument.
required = "="
if nargs == 0:
required = ""
# Pair short options with long options
# We need to do this because fish doesn't support multiple short
# or long options.
# However, since we are paring options only, this is fine
short = [f[1:] for f in flags if f.startswith("-") and len(f) == 2]
long = [f[2:] for f in flags if f.startswith("--")]
while len(short) > 0 and len(long) > 0:
arg = "%s/%s%s" % (short.pop(), long.pop(), required)
while len(short) > 0:
arg = "%s/%s" % (short.pop(), required)
while len(long) > 0:
arg = "%s%s" % (long.pop(), required)
args.append(arg)
# Even if there is no option, we still set variable.
# In fish such variable is an empty array, we use it to
# indicate that such subcommand exists.
args = " ".join(args)
return "set -g %s %s\n" % (optspec_var, args)
@staticmethod
def complete_head(
prog: str, index: Optional[int] = None, nargs: Optional[Union[int, str]] = None
) -> str:
"""Return the head of the completion command.
Args:
prog: Program name.
index: Index of positional argument.
nargs: Number of arguments.
Returns:
Head of the completion command.
"""
# Split command and subcommand
s = prog.split(None, 1)
subcmd = s[1] if len(s) == 2 else ""
if index is None:
return "complete -c %s -n '__fish_spack_using_command %s'" % (s[0], subcmd)
elif nargs in [argparse.ZERO_OR_MORE, argparse.ONE_OR_MORE, argparse.REMAINDER]:
head = "complete -c %s -n '__fish_spack_using_command_pos_remainder %d %s'"
else:
head = "complete -c %s -n '__fish_spack_using_command_pos %d %s'"
return head % (s[0], index, subcmd)
def complete(
self,
prog: str,
positionals: List[Tuple[str, Optional[Iterable[Any]], Union[int, str, None], str]],
optionals: List[Tuple[Sequence[str], List[str], str, Union[int, str, None], str]],
subcommands: List[Tuple[ArgumentParser, str, str]],
) -> str:
"""Return all the completion commands.
Args:
prog: Program name.
positionals: List of positional arguments.
optionals: List of optional arguments.
subcommands: List of subcommand parsers.
Returns:
Completion command.
"""
commands = []
if positionals:
commands.append(self.positionals(prog, positionals))
if subcommands:
commands.append(self.subcommands(prog, subcommands))
if optionals:
commands.append(self.optionals(prog, optionals))
return "".join(commands)
def positionals(
self,
prog: str,
positionals: List[Tuple[str, Optional[Iterable[Any]], Union[int, str, None], str]],
) -> str:
"""Return the completion for positional arguments.
Args:
prog: Program name.
positionals: List of positional arguments.
Returns:
Completion command.
"""
commands = []
for idx, (args, choices, nargs, help) in enumerate(positionals):
# Make sure we always get same order of output
if isinstance(choices, dict):
choices = sorted(choices.keys())
elif isinstance(choices, (set, frozenset)):
choices = sorted(choices)
# Remove platform-specific choices to avoid hard-coding the platform.
if choices is not None:
valid_choices = []
for choice in choices:
if spack.platforms.host().name not in choice:
valid_choices.append(choice)
choices = valid_choices
head = self.complete_head(prog, idx, nargs)
if choices is not None:
# If there are choices, we provide a completion for all possible values.
commands.append(head + " -f -a %s" % self._quote(" ".join(choices)))
else:
# Otherwise, we try to find a predefined completion for it
value = _fish_dest_get_complete(prog, args)
if value is not None:
commands.append(head + " " + value)
return "\n".join(commands) + "\n"
def prog_comment(self, prog: str) -> str:
"""Return a comment line for the command.
Args:
prog: Program name.
Returns:
Comment line.
"""
return "\n# %s\n" % prog
def optionals(
self,
prog: str,
optionals: List[Tuple[Sequence[str], List[str], str, Union[int, str, None], str]],
) -> str:
"""Return the completion for optional arguments.
Args:
prog: Program name.
optionals: List of optional arguments.
Returns:
Completion command.
"""
commands = []
head = self.complete_head(prog)
for flags, dest, _, nargs, help in optionals:
# Make sure we always get same order of output
if isinstance(dest, dict):
dest = sorted(dest.keys())
elif isinstance(dest, (set, frozenset)):
dest = sorted(dest)
# Remove platform-specific choices to avoid hard-coding the platform.
if dest is not None:
valid_choices = []
for choice in dest:
if spack.platforms.host().name not in choice:
valid_choices.append(choice)
dest = valid_choices
# To provide description for optionals, and also possible values,
# we need to use two split completion command.
# Otherwise, each option will have same description.
prefix = head
# Add all flags to the completion
for f in flags:
if f.startswith("--"):
long = f[2:]
prefix += " -l %s" % long
elif f.startswith("-"):
short = f[1:]
assert len(short) == 1
prefix += " -s %s" % short
# Check if option require argument.
# Currently multi-argument options are not supported, so we treat it like one argument.
if nargs != 0:
prefix += " -r"
if dest is not None:
# If there are choices, we provide a completion for all possible values.
commands.append(prefix + " -f -a %s" % self._quote(" ".join(dest)))
else:
# Otherwise, we try to find a predefined completion for it
value = _fish_dest_get_complete(prog, dest)
if value is not None:
commands.append(prefix + " " + value)
if help:
commands.append(prefix + " -d %s" % self._quote(help))
return "\n".join(commands) + "\n"
def subcommands(self, prog: str, subcommands: List[Tuple[ArgumentParser, str, str]]) -> str:
"""Return the completion for subcommands.
Args:
prog: Program name.
subcommands: List of subcommand parsers.
Returns:
Completion command.
"""
commands = []
head = self.complete_head(prog, 0)
for _, subcommand, help in subcommands:
command = head + " -f -a %s" % self._quote(subcommand)
if help is not None and len(help) > 0:
help = help.split("\n")[0]
command += " -d %s" % self._quote(help)
commands.append(command)
return "\n".join(commands) + "\n"
@formatter
def subcommands(args: Namespace, out: IO) -> None:
"""Hierarchical tree of subcommands.
@ -371,6 +816,15 @@ def bash(args: Namespace, out: IO) -> None:
writer.write(parser)
@formatter
def fish(args, out):
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
writer = FishCompletionWriter(parser.prog, out, args.aliases)
writer.write(parser)
def prepend_header(args: Namespace, out: IO) -> None:
"""Prepend header text at the beginning of a file.

View file

@ -253,12 +253,12 @@ def _configure_mirror(args):
def mirror_set(args):
"""Configure the connection details of a mirror"""
"""configure the connection details of a mirror"""
_configure_mirror(args)
def mirror_set_url(args):
"""Change the URL of a mirror."""
"""change the URL of a mirror"""
_configure_mirror(args)

View file

@ -14,7 +14,7 @@
import spack.cmd
import spack.main
import spack.paths
from spack.cmd.commands import _positional_to_subroutine
from spack.cmd.commands import _dest_to_fish_complete, _positional_to_subroutine
commands = spack.main.SpackCommand("commands", subprocess=True)
@ -185,26 +185,59 @@ def test_bash_completion():
assert "_spack_compiler_add() {" in out2
def test_update_completion_arg(tmpdir, monkeypatch):
def test_fish_completion():
"""Test the fish completion writer."""
out1 = commands("--format=fish")
# Make sure header not included
assert "function __fish_spack_argparse" not in out1
assert "complete -c spack --erase" not in out1
# Make sure subcommands appear
assert "__fish_spack_using_command remove" in out1
assert "__fish_spack_using_command compiler find" in out1
# Make sure aliases don't appear
assert "__fish_spack_using_command rm" not in out1
assert "__fish_spack_using_command compiler add" not in out1
# Make sure options appear
assert "-s h -l help" in out1
# Make sure subcommands are called
for complete_cmd in _dest_to_fish_complete.values():
assert complete_cmd in out1
out2 = commands("--aliases", "--format=fish")
# Make sure aliases appear
assert "__fish_spack_using_command rm" in out2
assert "__fish_spack_using_command compiler add" in out2
@pytest.mark.parametrize("shell", ["bash", "fish"])
def test_update_completion_arg(shell, tmpdir, monkeypatch):
"""Test the update completion flag."""
mock_infile = tmpdir.join("spack-completion.in")
mock_bashfile = tmpdir.join("spack-completion.bash")
mock_outfile = tmpdir.join(f"spack-completion.{shell}")
mock_args = {
"bash": {
shell: {
"aliases": True,
"format": "bash",
"format": shell,
"header": str(mock_infile),
"update": str(mock_bashfile),
"update": str(mock_outfile),
}
}
# make a mock completion file missing the --update-completion argument
real_args = spack.cmd.commands.update_completion_args
shutil.copy(real_args["bash"]["header"], mock_args["bash"]["header"])
with open(real_args["bash"]["update"]) as old:
shutil.copy(real_args[shell]["header"], mock_args[shell]["header"])
with open(real_args[shell]["update"]) as old:
old_file = old.read()
with open(mock_args["bash"]["update"], "w") as mock:
mock.write(old_file.replace("--update-completion", ""))
with open(mock_args[shell]["update"], "w") as mock:
mock.write(old_file.replace("update-completion", ""))
monkeypatch.setattr(spack.cmd.commands, "update_completion_args", mock_args)
@ -214,16 +247,17 @@ def test_update_completion_arg(tmpdir, monkeypatch):
local_commands("--update-completion", "-a")
# ensure arg is restored
assert "--update-completion" not in mock_bashfile.read()
assert "update-completion" not in mock_outfile.read()
local_commands("--update-completion")
assert "--update-completion" in mock_bashfile.read()
assert "update-completion" in mock_outfile.read()
# Note: this test is never expected to be supported on Windows
@pytest.mark.skipif(
sys.platform == "win32", reason="bash completion script generator fails on windows"
sys.platform == "win32", reason="shell completion script generator fails on windows"
)
def test_updated_completion_scripts(tmpdir):
@pytest.mark.parametrize("shell", ["bash", "fish"])
def test_updated_completion_scripts(shell, tmpdir):
"""Make sure our shell tab completion scripts remain up-to-date."""
msg = (
@ -233,12 +267,11 @@ def test_updated_completion_scripts(tmpdir):
"and adding the changed files to your pull request."
)
for shell in ["bash"]: # 'zsh', 'fish']:
header = os.path.join(spack.paths.share_path, shell, "spack-completion.in")
script = "spack-completion.{0}".format(shell)
old_script = os.path.join(spack.paths.share_path, script)
new_script = str(tmpdir.join(script))
header = os.path.join(spack.paths.share_path, shell, "spack-completion.in")
script = "spack-completion.{0}".format(shell)
old_script = os.path.join(spack.paths.share_path, script)
new_script = str(tmpdir.join(script))
commands("--aliases", "--format", shell, "--header", header, "--update", new_script)
commands("--aliases", "--format", shell, "--header", header, "--update", new_script)
assert filecmp.cmp(old_script, new_script), msg
assert filecmp.cmp(old_script, new_script), msg

View file

@ -22,13 +22,3 @@
def test_format_not_overridden():
with pytest.raises(TypeError):
aw.ArgparseWriter("spack")
def test_completion_format_not_overridden():
writer = aw.ArgparseCompletionWriter("spack")
assert writer.positionals([]) == ""
assert writer.optionals([]) == ""
assert writer.subcommands([]) == ""
writer.write(parser)

View file

@ -0,0 +1,347 @@
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
# NOTE: spack-completion.fish is auto-generated by:
#
# $ spack commands --aliases --format=fish
# --header=fish/spack-completion.in --update=spack-completion.fish
#
# Please do not manually modify this file.
# Check fish version before proceeding
set -l fish_version (string split '.' $FISH_VERSION)
if test $fish_version[1] -lt 3
if test $fish_version[1] -eq 3
and test $fish_version[2] -lt 2
echo 'Fish version is older than 3.2.0. Some completion features may not work'
set -g __fish_spack_force_files
else
echo 'This script requires fish version 3.0 or later'
exit 1
end
else
set -g __fish_spack_force_files -F
end
# The following global variables are used as a cache of `__fish_spack_argparse`
# Cached command line
set -g __fish_spack_argparse_cache_line
# Parsed command
set -g __fish_spack_argparse_command
# Remaining arguments
set -g __fish_spack_argparse_argv
# Return value
set -g __fish_spack_argparse_return
# Spack command generates an optspec variable $__fish_spack_optspecs_<command>.
# We check if this command exists, and echo the optspec variable name.
function __fish_spack_get_optspecs -d 'Get optspecs of spack command'
# Convert arguments to replace ' ' and '-' by '_'
set -l cmd_var (string replace -ra -- '[ -]' '_' $argv | string join '_')
# Set optspec variable name
set -l optspecs_var __fish_spack_optspecs_$cmd_var
# Query if variable $$optspecs_var exists
set -q $optspecs_var; or return 1
# If it exists, echo all optspecs line by line.
# String join returns 1 if no join was performed, so we return 0 in such case.
string join \n $$optspecs_var; or return 0
end
# Parse command-line arguments, save results to global variables,
# and add found flags to __fish_spack_flag_<flag>.
# Returns 1 if help flag is found.
function __fish_spack_argparse
# Figure out if the current invocation already has a command.
set -l args $argv
set -l commands
# Return cached result if arguments haven't changed
if test "$__fish_spack_argparse_cache_line" = "$args"
return $__fish_spack_argparse_return
end
# Clear all flags found in last run
set -g | string replace -rf -- '^(__fish_spack_flag_\w+)(.*?)$' 'set -ge $1' | source
# Set default return value to 0, indicating success
set -g __fish_spack_argparse_return 0
# Set command line to current arguments
set -g __fish_spack_argparse_cache_line $argv
# Recursively check arguments for commands
while set -q args[1]
# Get optspecs of current command
set -l optspecs (__fish_spack_get_optspecs $commands $args[1])
or break
# If command exists, shift arguments
set -a commands $args[1]
set -e args[1]
# If command has no arguments, continue
set -q optspecs[1]; or continue
# Parse arguments. Set variable _flag_<flag> if flag is found.
# We find all these variables and set them to the global variable __fish_spack_flag_<flag>.
argparse -i -s $optspecs -- $args 2>/dev/null; or break
set -l | string replace -rf -- '^(_flag_.*)$' 'set -g __fish_spack$1' | source
# Set args to not parsed arguments
set args $argv
# If command has help flag, we don't need to parse more so short circuit
if set -q _flag_help
set -g __fish_spack_argparse_return 1
break
end
end
# Set cached variables
set -g __fish_spack_argparse_command $commands
set -g __fish_spack_argparse_argv $args
return $__fish_spack_argparse_return
end
# Check if current commandline's command is "spack $argv"
function __fish_spack_using_command
set -l line (commandline -opc)
__fish_spack_argparse $line; or return 1
set -p argv spack
test "$__fish_spack_argparse_command" = "$argv"
end
# Check if current commandline's command is "spack $argv[2..-1]",
# and cursor is at $argv[1]-th positional argument
function __fish_spack_using_command_pos
__fish_spack_using_command $argv[2..-1]
or return
test (count $__fish_spack_argparse_argv) -eq $argv[1]
end
function __fish_spack_using_command_pos_remainder
__fish_spack_using_command $argv[2..-1]
or return
test (count $__fish_spack_argparse_argv) -ge $argv[1]
end
# Helper functions for subcommands
function __fish_spack_bootstrap_names
if set -q __fish_spack_flag_scope
spack bootstrap list --scope $__fish_spack_flag_scope | string replace -rf -- '^Name: (\w+).*?$' '$1'
else
spack bootstrap list | string replace -rf -- '^Name: (\w+).*?$' '$1'
end
end
# Reference: sudo's fish completion
function __fish_spack_build_env_spec
set token (commandline -opt)
set -l index (contains -- -- $__fish_spack_argparse_argv)
if set -q index[1]
__fish_complete_subcommand --commandline $__fish_spack_argparse_argv[(math $index + 1)..-1]
else if set -q __fish_spack_argparse_argv[1]
__fish_complete_subcommand --commandline "$__fish_spack_argparse_argv[2..-1] $token"
else
__fish_spack_specs
end
end
function __fish_spack_commands
spack commands
end
function __fish_spack_colon_path
set token (string split -rm1 ':' (commandline -opt))
if test (count $token) -lt 2
__fish_complete_path $token[1]
else
__fish_complete_path $token[2] | string replace -r -- '^' "$token[1]:"
end
end
function __fish_spack_config_sections
if set -q __fish_spack_flag_scope
spack config --scope $__fish_spack_flag_scope list | string split ' '
else
spack config list | string split ' '
end
end
function __fish_spack_environments
string trim (spack env list)
end
function __fish_spack_extensions
# Skip optional flags, or it will be really slow
string match -q -- '-*' (commandline -opt)
and return
comm -1 -2 (spack extensions | string trim | psub) (__fish_spack_installed_packages | sort | psub)
end
function __fish_spack_gpg_keys
spack gpg list
end
function __fish_spack_installed_compilers
spack compilers | grep -v '^[=-]\|^$'
end
function __fish_spack_installed_packages
spack find --no-groups --format '{name}' | uniq
end
function __fish_spack_installed_specs
# Try match local hash first
__fish_spack_installed_specs_id
and return
spack find --no-groups --format '{name}@{version}'
end
function __fish_spack_installed_specs_id
set -l token (commandline -opt)
string match -q -- '/*' $token
or return 1
spack find --format '/{hash:7}'\t'{name}{@version}'
end
function __fish_spack_git_rev
type -q __fish_git_ranges
and __fish_git_ranges
end
function __fish_spack_mirrors
spack mirror list | awk {'printf ("%s\t%s", $1, $2)'}
end
function __fish_spack_package_versions
string trim (spack versions $argv)
end
function __fish_spack_packages
spack list
end
function __fish_spack_pkg_packages
spack pkg list
end
function __fish_spack_providers
string trim (spack providers | grep -v '^$')
end
function __fish_spack_repos
spack repo list | awk {'printf ("%s\t%s", $1, $2)'}
end
function __fish_spack_scopes
# TODO: how to list all scopes?
set -l scope system site user defaults
set -l platform cray darwin linux test
string join \n $scope
end
function __fish_spack_specs
set -l token (commandline -opt)
# Complete compilers
if string match -rq -- '^(?<pre>.*%)[\w-]*(@[\w\.+~-]*)?$' $token
__fish_spack_installed_compilers | string replace -r -- '^' "$pre"
return
end
# Try to complete spec version
# Currently we can only match '@' after a package name
set -l package
# Match ^ following package name
if string match -rq -- '^(?<pre>.*?\^)[\w\.+~-]*$' $token
# Package name is the nearest, assuming first character is always a letter or digit
set packages (string match -ar -- '^[\w-]+' $__fish_spack_argparse_argv $token)
set package $packages[-1]
if test -n "$package"
spack dependencies $package | string replace -r -- '^' "$pre"
return
end
end
# Match @ following package name
if string match -rq -- '^(?<pre>.*?\^?(?<packages>[\w\.+~-]*)@)[\w\.]*$' $token
set package $packages[-1]
# Matched @ starting at next token
if test -z "$package"
string match -arq -- '(^|\^)(?<inners>[\w\.+~-]*)$' $__fish_spack_argparse_argv[-1]
if test -n "$inners[1]"
set package $inners[-1]
end
end
end
# Complete version if package found
if test -n "$package"
# Only list safe versions for speed
string trim (spack versions --safe $package) | string replace -r -- '^' "$pre"
return
end
# Else complete package name
__fish_spack_installed_packages | string replace -r -- '$' \t"installed"
spack list
end
function __fish_spack_specs_or_id
# Try to match local hash first
__fish_spack_installed_specs_id
and return
__fish_spack_specs
end
function __fish_spack_tags
string trim (spack tags)
end
function __fish_spack_tests
spack test list | grep -v '^[=-]'
end
function __fish_spack_unit_tests
# Skip optional flags, or it will be really slow
string match -q -- '-*' (commandline -opt)
and return
spack unit-test -l
end
function __fish_spack_yamls
# Trim flag from current token
string match -rq -- '(?<pre>-.)?(?<token>.*)' (commandline -opt)
if test -n "$token"
find $token* -type f '(' -iname '*.yaml' -or -iname '*.yml' ')'
else
find -maxdepth 2 -type f '(' -iname '*.yaml' -or -iname '*.yml' ')' | cut -c 3-
end
end
# Reset existing completions
complete -c spack --erase
# Spack commands
#
# Everything below here is auto-generated.

View file

@ -785,7 +785,15 @@ if test -z "$SPACK_SKIP_MODULES"
sp_multi_pathadd MODULEPATH $_sp_tcl_roots
end
# Add programmable tab completion for fish
#
set -l fish_version (string split '.' $FISH_VERSION)
if test $fish_version[1] -gt 3
or test $fish_version[1] -eq 3
and test $fish_version[2] -ge 2
source $sp_share_dir/spack-completion.fish
end
#
# NOTES

3029
share/spack/spack-completion.fish Executable file

File diff suppressed because it is too large Load diff