File: complete.py

package info (click to toggle)
python-invoke 2.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,856 kB
  • sloc: python: 15,986; makefile: 24
file content (129 lines) | stat: -rw-r--r-- 5,222 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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))
        )