Initial commit (Clean history)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
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))
|
||||
)
|
||||
Reference in New Issue
Block a user