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:
parent
66e85ae39a
commit
90ac0ef66e
@ -9,7 +9,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from typing import IO, Optional, Sequence, Tuple
|
from typing import IO, Any, Iterable, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
@ -25,9 +25,9 @@ def __init__(
|
|||||||
prog: str,
|
prog: str,
|
||||||
description: Optional[str],
|
description: Optional[str],
|
||||||
usage: str,
|
usage: str,
|
||||||
positionals: Sequence[Tuple[str, str]],
|
positionals: List[Tuple[str, Optional[Iterable[Any]], Union[int, str, None], str]],
|
||||||
optionals: Sequence[Tuple[Sequence[str], str, str]],
|
optionals: List[Tuple[Sequence[str], List[str], str, Union[int, str, None], str]],
|
||||||
subcommands: Sequence[Tuple[ArgumentParser, str]],
|
subcommands: List[Tuple[ArgumentParser, str, str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a new Command instance.
|
"""Initialize a new Command instance.
|
||||||
|
|
||||||
@ -96,13 +96,30 @@ def parse(self, parser: ArgumentParser, prog: str) -> Command:
|
|||||||
if action.option_strings:
|
if action.option_strings:
|
||||||
flags = action.option_strings
|
flags = action.option_strings
|
||||||
dest_flags = fmt._format_action_invocation(action)
|
dest_flags = fmt._format_action_invocation(action)
|
||||||
help = self._expand_help(action) if action.help else ""
|
nargs = action.nargs
|
||||||
help = help.replace("\n", " ")
|
help = (
|
||||||
optionals.append((flags, dest_flags, 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):
|
elif isinstance(action, argparse._SubParsersAction):
|
||||||
for subaction in action._choices_actions:
|
for subaction in action._choices_actions:
|
||||||
subparser = action._name_parser_map[subaction.dest]
|
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, ...)'
|
# Look for aliases of the form 'name (alias, ...)'
|
||||||
if self.aliases and isinstance(subaction.metavar, str):
|
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(", ")
|
aliases = match.group(2).split(", ")
|
||||||
for alias in aliases:
|
for alias in aliases:
|
||||||
subparser = action._name_parser_map[alias]
|
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:
|
else:
|
||||||
args = fmt._format_action_invocation(action)
|
args = fmt._format_action_invocation(action)
|
||||||
help = self._expand_help(action) if action.help else ""
|
help = (
|
||||||
help = help.replace("\n", " ")
|
self._expand_help(action)
|
||||||
positionals.append((args, help))
|
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)
|
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)
|
cmd = self.parse(parser, prog)
|
||||||
self.out.write(self.format(cmd))
|
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)
|
self._write(subparser, prog, level=level + 1)
|
||||||
|
|
||||||
def write(self, parser: ArgumentParser) -> None:
|
def write(self, parser: ArgumentParser) -> None:
|
||||||
@ -205,13 +232,13 @@ def format(self, cmd: Command) -> str:
|
|||||||
|
|
||||||
if cmd.positionals:
|
if cmd.positionals:
|
||||||
string.write(self.begin_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.positional(args, help))
|
||||||
string.write(self.end_positionals())
|
string.write(self.end_positionals())
|
||||||
|
|
||||||
if cmd.optionals:
|
if cmd.optionals:
|
||||||
string.write(self.begin_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.optional(dest_flags, help))
|
||||||
string.write(self.end_optionals())
|
string.write(self.end_optionals())
|
||||||
|
|
||||||
@ -338,7 +365,7 @@ def end_optionals(self) -> str:
|
|||||||
"""
|
"""
|
||||||
return ""
|
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.
|
"""Table with links to other subcommands.
|
||||||
|
|
||||||
Arguments:
|
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)
|
prog = re.sub(r"^[^ ]* ", "", cmd.prog)
|
||||||
string += " * :ref:`{0} <{1}>`\n".format(prog, cmd.prog.replace(" ", "-"))
|
string += " * :ref:`{0} <{1}>`\n".format(prog, cmd.prog.replace(" ", "-"))
|
||||||
|
|
||||||
return string + "\n"
|
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 ""
|
|
||||||
|
@ -9,16 +9,11 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser, Namespace
|
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.filesystem as fs
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
from llnl.util.argparsewriter import (
|
from llnl.util.argparsewriter import ArgparseRstWriter, ArgparseWriter, Command
|
||||||
ArgparseCompletionWriter,
|
|
||||||
ArgparseRstWriter,
|
|
||||||
ArgparseWriter,
|
|
||||||
Command,
|
|
||||||
)
|
|
||||||
from llnl.util.tty.colify import colify
|
from llnl.util.tty.colify import colify
|
||||||
|
|
||||||
import spack.cmd
|
import spack.cmd
|
||||||
@ -43,7 +38,13 @@
|
|||||||
"format": "bash",
|
"format": "bash",
|
||||||
"header": os.path.join(spack.paths.share_path, "bash", "spack-completion.in"),
|
"header": os.path.join(spack.paths.share_path, "bash", "spack-completion.in"),
|
||||||
"update": os.path.join(spack.paths.share_path, "spack-completion.bash"),
|
"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."""
|
"""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(
|
def body(
|
||||||
self, positionals: Sequence[str], optionals: Sequence[str], subcommands: Sequence[str]
|
self, positionals: Sequence[str], optionals: Sequence[str], subcommands: Sequence[str]
|
||||||
) -> str:
|
) -> str:
|
||||||
@ -264,6 +319,396 @@ def subcommands(self, subcommands: Sequence[str]) -> str:
|
|||||||
return 'SPACK_COMPREPLY="{0}"'.format(" ".join(subcommands))
|
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
|
@formatter
|
||||||
def subcommands(args: Namespace, out: IO) -> None:
|
def subcommands(args: Namespace, out: IO) -> None:
|
||||||
"""Hierarchical tree of subcommands.
|
"""Hierarchical tree of subcommands.
|
||||||
@ -371,6 +816,15 @@ def bash(args: Namespace, out: IO) -> None:
|
|||||||
writer.write(parser)
|
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:
|
def prepend_header(args: Namespace, out: IO) -> None:
|
||||||
"""Prepend header text at the beginning of a file.
|
"""Prepend header text at the beginning of a file.
|
||||||
|
|
||||||
|
@ -253,12 +253,12 @@ def _configure_mirror(args):
|
|||||||
|
|
||||||
|
|
||||||
def mirror_set(args):
|
def mirror_set(args):
|
||||||
"""Configure the connection details of a mirror"""
|
"""configure the connection details of a mirror"""
|
||||||
_configure_mirror(args)
|
_configure_mirror(args)
|
||||||
|
|
||||||
|
|
||||||
def mirror_set_url(args):
|
def mirror_set_url(args):
|
||||||
"""Change the URL of a mirror."""
|
"""change the URL of a mirror"""
|
||||||
_configure_mirror(args)
|
_configure_mirror(args)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
import spack.cmd
|
import spack.cmd
|
||||||
import spack.main
|
import spack.main
|
||||||
import spack.paths
|
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)
|
commands = spack.main.SpackCommand("commands", subprocess=True)
|
||||||
|
|
||||||
@ -185,26 +185,59 @@ def test_bash_completion():
|
|||||||
assert "_spack_compiler_add() {" in out2
|
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_infile = tmpdir.join("spack-completion.in")
|
||||||
mock_bashfile = tmpdir.join("spack-completion.bash")
|
mock_outfile = tmpdir.join(f"spack-completion.{shell}")
|
||||||
|
|
||||||
mock_args = {
|
mock_args = {
|
||||||
"bash": {
|
shell: {
|
||||||
"aliases": True,
|
"aliases": True,
|
||||||
"format": "bash",
|
"format": shell,
|
||||||
"header": str(mock_infile),
|
"header": str(mock_infile),
|
||||||
"update": str(mock_bashfile),
|
"update": str(mock_outfile),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# make a mock completion file missing the --update-completion argument
|
# make a mock completion file missing the --update-completion argument
|
||||||
real_args = spack.cmd.commands.update_completion_args
|
real_args = spack.cmd.commands.update_completion_args
|
||||||
shutil.copy(real_args["bash"]["header"], mock_args["bash"]["header"])
|
shutil.copy(real_args[shell]["header"], mock_args[shell]["header"])
|
||||||
with open(real_args["bash"]["update"]) as old:
|
with open(real_args[shell]["update"]) as old:
|
||||||
old_file = old.read()
|
old_file = old.read()
|
||||||
with open(mock_args["bash"]["update"], "w") as mock:
|
with open(mock_args[shell]["update"], "w") as mock:
|
||||||
mock.write(old_file.replace("--update-completion", ""))
|
mock.write(old_file.replace("update-completion", ""))
|
||||||
|
|
||||||
monkeypatch.setattr(spack.cmd.commands, "update_completion_args", mock_args)
|
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")
|
local_commands("--update-completion", "-a")
|
||||||
|
|
||||||
# ensure arg is restored
|
# ensure arg is restored
|
||||||
assert "--update-completion" not in mock_bashfile.read()
|
assert "update-completion" not in mock_outfile.read()
|
||||||
local_commands("--update-completion")
|
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
|
# Note: this test is never expected to be supported on Windows
|
||||||
@pytest.mark.skipif(
|
@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."""
|
"""Make sure our shell tab completion scripts remain up-to-date."""
|
||||||
|
|
||||||
msg = (
|
msg = (
|
||||||
@ -233,12 +267,11 @@ def test_updated_completion_scripts(tmpdir):
|
|||||||
"and adding the changed files to your pull request."
|
"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")
|
||||||
header = os.path.join(spack.paths.share_path, shell, "spack-completion.in")
|
script = "spack-completion.{0}".format(shell)
|
||||||
script = "spack-completion.{0}".format(shell)
|
old_script = os.path.join(spack.paths.share_path, script)
|
||||||
old_script = os.path.join(spack.paths.share_path, script)
|
new_script = str(tmpdir.join(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
|
||||||
|
@ -22,13 +22,3 @@
|
|||||||
def test_format_not_overridden():
|
def test_format_not_overridden():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
aw.ArgparseWriter("spack")
|
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)
|
|
||||||
|
347
share/spack/fish/spack-completion.in
Normal file
347
share/spack/fish/spack-completion.in
Normal 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.
|
@ -785,7 +785,15 @@ if test -z "$SPACK_SKIP_MODULES"
|
|||||||
sp_multi_pathadd MODULEPATH $_sp_tcl_roots
|
sp_multi_pathadd MODULEPATH $_sp_tcl_roots
|
||||||
end
|
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
|
# NOTES
|
||||||
|
3029
share/spack/spack-completion.fish
Executable file
3029
share/spack/spack-completion.fish
Executable file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user