130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
"""
|
|
Command-line completion mechanisms, executed by the core ``--complete`` flag.
|
|
"""
|
|
|
|
from typing import List
|
|
import glob
|
|
import os
|
|
import re
|
|
import shlex
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ..exceptions import Exit, ParseError
|
|
from ..util import debug, task_name_sort_key
|
|
|
|
if TYPE_CHECKING:
|
|
from ..collection import Collection
|
|
from ..parser import Parser, ParseResult, ParserContext
|
|
|
|
|
|
def complete(
|
|
names: List[str],
|
|
core: "ParseResult",
|
|
initial_context: "ParserContext",
|
|
collection: "Collection",
|
|
parser: "Parser",
|
|
) -> Exit:
|
|
# Strip out program name (scripts give us full command line)
|
|
# TODO: this may not handle path/to/script though?
|
|
invocation = re.sub(r"^({}) ".format("|".join(names)), "", core.remainder)
|
|
debug("Completing for invocation: {!r}".format(invocation))
|
|
# Tokenize (shlex will have to do)
|
|
tokens = shlex.split(invocation)
|
|
# Handle flags (partial or otherwise)
|
|
if tokens and tokens[-1].startswith("-"):
|
|
tail = tokens[-1]
|
|
debug("Invocation's tail {!r} is flag-like".format(tail))
|
|
# Gently parse invocation to obtain 'current' context.
|
|
# Use last seen context in case of failure (required for
|
|
# otherwise-invalid partial invocations being completed).
|
|
|
|
contexts: List[ParserContext]
|
|
try:
|
|
debug("Seeking context name in tokens: {!r}".format(tokens))
|
|
contexts = parser.parse_argv(tokens)
|
|
except ParseError as e:
|
|
msg = "Got parser error ({!r}), grabbing its last-seen context {!r}" # noqa
|
|
debug(msg.format(e, e.context))
|
|
contexts = [e.context] if e.context is not None else []
|
|
# Fall back to core context if no context seen.
|
|
debug("Parsed invocation, contexts: {!r}".format(contexts))
|
|
if not contexts or not contexts[-1]:
|
|
context = initial_context
|
|
else:
|
|
context = contexts[-1]
|
|
debug("Selected context: {!r}".format(context))
|
|
# Unknown flags (could be e.g. only partially typed out; could be
|
|
# wholly invalid; doesn't matter) complete with flags.
|
|
debug("Looking for {!r} in {!r}".format(tail, context.flags))
|
|
if tail not in context.flags:
|
|
debug("Not found, completing with flag names")
|
|
# Long flags - partial or just the dashes - complete w/ long flags
|
|
if tail.startswith("--"):
|
|
for name in filter(
|
|
lambda x: x.startswith("--"), context.flag_names()
|
|
):
|
|
print(name)
|
|
# Just a dash, completes with all flags
|
|
elif tail == "-":
|
|
for name in context.flag_names():
|
|
print(name)
|
|
# Otherwise, it's something entirely invalid (a shortflag not
|
|
# recognized, or a java style flag like -foo) so return nothing
|
|
# (the shell will still try completing with files, but that doesn't
|
|
# hurt really.)
|
|
else:
|
|
pass
|
|
# Known flags complete w/ nothing or tasks, depending
|
|
else:
|
|
# Flags expecting values: do nothing, to let default (usually
|
|
# file) shell completion occur (which we actively want in this
|
|
# case.)
|
|
if context.flags[tail].takes_value:
|
|
debug("Found, and it takes a value, so no completion")
|
|
pass
|
|
# Not taking values (eg bools): print task names
|
|
else:
|
|
debug("Found, takes no value, printing task names")
|
|
print_task_names(collection)
|
|
# If not a flag, is either task name or a flag value, so just complete
|
|
# task names.
|
|
else:
|
|
debug("Last token isn't flag-like, just printing task names")
|
|
print_task_names(collection)
|
|
raise Exit
|
|
|
|
|
|
def print_task_names(collection: "Collection") -> None:
|
|
for name in sorted(collection.task_names, key=task_name_sort_key):
|
|
print(name)
|
|
# Just stick aliases after the thing they're aliased to. Sorting isn't
|
|
# so important that it's worth bending over backwards here.
|
|
for alias in collection.task_names[name]:
|
|
print(alias)
|
|
|
|
|
|
def print_completion_script(shell: str, names: List[str]) -> None:
|
|
# Grab all .completion files in invoke/completion/. (These used to have no
|
|
# suffix, but surprise, that's super fragile.
|
|
completions = {
|
|
os.path.splitext(os.path.basename(x))[0]: x
|
|
for x in glob.glob(
|
|
os.path.join(
|
|
os.path.dirname(os.path.realpath(__file__)), "*.completion"
|
|
)
|
|
)
|
|
}
|
|
try:
|
|
path = completions[shell]
|
|
except KeyError:
|
|
err = 'Completion for shell "{}" not supported (options are: {}).'
|
|
raise ParseError(err.format(shell, ", ".join(sorted(completions))))
|
|
debug("Printing completion script from {}".format(path))
|
|
# Choose one arbitrary program name for script's own internal invocation
|
|
# (also used to construct completion function names when necessary)
|
|
binary = names[0]
|
|
with open(path, "r") as script:
|
|
print(
|
|
script.read().format(binary=binary, spaced_names=" ".join(names))
|
|
)
|