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