1"""
2Command-line completion mechanisms, executed by the core ``--complete`` flag.
3"""
4
5from typing import List
6import glob
7import os
8import re
9import shlex
10from typing import TYPE_CHECKING
11
12from ..exceptions import Exit, ParseError
13from ..util import debug, task_name_sort_key
14
15if TYPE_CHECKING:
16 from ..collection import Collection
17 from ..parser import Parser, ParseResult, ParserContext
18
19
20def complete(
21 names: List[str],
22 core: "ParseResult",
23 initial_context: "ParserContext",
24 collection: "Collection",
25 parser: "Parser",
26) -> Exit:
27 # Strip out program name (scripts give us full command line)
28 # TODO: this may not handle path/to/script though?
29 invocation = re.sub(r"^({}) ".format("|".join(names)), "", core.remainder)
30 debug("Completing for invocation: {!r}".format(invocation))
31 # Tokenize (shlex will have to do)
32 tokens = shlex.split(invocation)
33 # Handle flags (partial or otherwise)
34 if tokens and tokens[-1].startswith("-"):
35 tail = tokens[-1]
36 debug("Invocation's tail {!r} is flag-like".format(tail))
37 # Gently parse invocation to obtain 'current' context.
38 # Use last seen context in case of failure (required for
39 # otherwise-invalid partial invocations being completed).
40
41 contexts: List[ParserContext]
42 try:
43 debug("Seeking context name in tokens: {!r}".format(tokens))
44 contexts = parser.parse_argv(tokens)
45 except ParseError as e:
46 msg = "Got parser error ({!r}), grabbing its last-seen context {!r}" # noqa
47 debug(msg.format(e, e.context))
48 contexts = [e.context] if e.context is not None else []
49 # Fall back to core context if no context seen.
50 debug("Parsed invocation, contexts: {!r}".format(contexts))
51 if not contexts or not contexts[-1]:
52 context = initial_context
53 else:
54 context = contexts[-1]
55 debug("Selected context: {!r}".format(context))
56 # Unknown flags (could be e.g. only partially typed out; could be
57 # wholly invalid; doesn't matter) complete with flags.
58 debug("Looking for {!r} in {!r}".format(tail, context.flags))
59 if tail not in context.flags:
60 debug("Not found, completing with flag names")
61 # Long flags - partial or just the dashes - complete w/ long flags
62 if tail.startswith("--"):
63 for name in filter(
64 lambda x: x.startswith("--"), context.flag_names()
65 ):
66 print(name)
67 # Just a dash, completes with all flags
68 elif tail == "-":
69 for name in context.flag_names():
70 print(name)
71 # Otherwise, it's something entirely invalid (a shortflag not
72 # recognized, or a java style flag like -foo) so return nothing
73 # (the shell will still try completing with files, but that doesn't
74 # hurt really.)
75 else:
76 pass
77 # Known flags complete w/ nothing or tasks, depending
78 else:
79 # Flags expecting values: do nothing, to let default (usually
80 # file) shell completion occur (which we actively want in this
81 # case.)
82 if context.flags[tail].takes_value:
83 debug("Found, and it takes a value, so no completion")
84 pass
85 # Not taking values (eg bools): print task names
86 else:
87 debug("Found, takes no value, printing task names")
88 print_task_names(collection)
89 # If not a flag, is either task name or a flag value, so just complete
90 # task names.
91 else:
92 debug("Last token isn't flag-like, just printing task names")
93 print_task_names(collection)
94 raise Exit
95
96
97def print_task_names(collection: "Collection") -> None:
98 for name in sorted(collection.task_names, key=task_name_sort_key):
99 print(name)
100 # Just stick aliases after the thing they're aliased to. Sorting isn't
101 # so important that it's worth bending over backwards here.
102 for alias in collection.task_names[name]:
103 print(alias)
104
105
106def print_completion_script(shell: str, names: List[str]) -> None:
107 # Grab all .completion files in invoke/completion/. (These used to have no
108 # suffix, but surprise, that's super fragile.
109 completions = {
110 os.path.splitext(os.path.basename(x))[0]: x
111 for x in glob.glob(
112 os.path.join(
113 os.path.dirname(os.path.realpath(__file__)), "*.completion"
114 )
115 )
116 }
117 try:
118 path = completions[shell]
119 except KeyError:
120 err = 'Completion for shell "{}" not supported (options are: {}).'
121 raise ParseError(err.format(shell, ", ".join(sorted(completions))))
122 debug("Printing completion script from {}".format(path))
123 # Choose one arbitrary program name for script's own internal invocation
124 # (also used to construct completion function names when necessary)
125 binary = names[0]
126 with open(path, "r") as script:
127 print(
128 script.read().format(binary=binary, spaced_names=" ".join(names))
129 )