|
| 1 | +# coding=utf-8 |
| 2 | +"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results""" |
| 3 | + |
| 4 | +try: |
| 5 | + # check if argcomplete is installed |
| 6 | + import argcomplete |
| 7 | +except ImportError: |
| 8 | + # not installed, skip the rest of the file |
| 9 | + pass |
| 10 | + |
| 11 | +else: |
| 12 | + # argcomplete is installed |
| 13 | + |
| 14 | + from contextlib import redirect_stdout |
| 15 | + import copy |
| 16 | + from io import StringIO |
| 17 | + import os |
| 18 | + import shlex |
| 19 | + import sys |
| 20 | + |
| 21 | + from . import constants |
| 22 | + from . import utils |
| 23 | + |
| 24 | + |
| 25 | + def tokens_for_completion(line, endidx): |
| 26 | + """ |
| 27 | + Used by tab completion functions to get all tokens through the one being completed |
| 28 | + :param line: str - the current input line with leading whitespace removed |
| 29 | + :param endidx: int - the ending index of the prefix text |
| 30 | + :return: A 4 item tuple where the items are |
| 31 | + On Success |
| 32 | + tokens: list of unquoted tokens |
| 33 | + this is generally the list needed for tab completion functions |
| 34 | + raw_tokens: list of tokens with any quotes preserved |
| 35 | + this can be used to know if a token was quoted or is missing a closing quote |
| 36 | + begidx: beginning of last token |
| 37 | + endidx: cursor position |
| 38 | +
|
| 39 | + Both lists are guaranteed to have at least 1 item |
| 40 | + The last item in both lists is the token being tab completed |
| 41 | +
|
| 42 | + On Failure |
| 43 | + Both items are None |
| 44 | + """ |
| 45 | + unclosed_quote = '' |
| 46 | + quotes_to_try = copy.copy(constants.QUOTES) |
| 47 | + |
| 48 | + tmp_line = line[:endidx] |
| 49 | + tmp_endidx = endidx |
| 50 | + |
| 51 | + # Parse the line into tokens |
| 52 | + while True: |
| 53 | + try: |
| 54 | + # Use non-POSIX parsing to keep the quotes around the tokens |
| 55 | + initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False) |
| 56 | + |
| 57 | + # calculate begidx |
| 58 | + if unclosed_quote: |
| 59 | + begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) + 1 |
| 60 | + else: |
| 61 | + if tmp_endidx > 0 and tmp_line[tmp_endidx - 1] == ' ': |
| 62 | + begidx = endidx |
| 63 | + else: |
| 64 | + begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) |
| 65 | + |
| 66 | + # If the cursor is at an empty token outside of a quoted string, |
| 67 | + # then that is the token being completed. Add it to the list. |
| 68 | + if not unclosed_quote and begidx == tmp_endidx: |
| 69 | + initial_tokens.append('') |
| 70 | + break |
| 71 | + except ValueError: |
| 72 | + # ValueError can be caused by missing closing quote |
| 73 | + if not quotes_to_try: |
| 74 | + # Since we have no more quotes to try, something else |
| 75 | + # is causing the parsing error. Return None since |
| 76 | + # this means the line is malformed. |
| 77 | + return None, None, None, None |
| 78 | + |
| 79 | + # Add a closing quote and try to parse again |
| 80 | + unclosed_quote = quotes_to_try[0] |
| 81 | + quotes_to_try = quotes_to_try[1:] |
| 82 | + |
| 83 | + tmp_line = line[:endidx] |
| 84 | + tmp_line += unclosed_quote |
| 85 | + tmp_endidx = endidx + 1 |
| 86 | + |
| 87 | + raw_tokens = initial_tokens |
| 88 | + |
| 89 | + # Save the unquoted tokens |
| 90 | + tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens] |
| 91 | + |
| 92 | + # If the token being completed had an unclosed quote, we need |
| 93 | + # to remove the closing quote that was added in order for it |
| 94 | + # to match what was on the command line. |
| 95 | + if unclosed_quote: |
| 96 | + raw_tokens[-1] = raw_tokens[-1][:-1] |
| 97 | + |
| 98 | + return tokens, raw_tokens, begidx, endidx |
| 99 | + |
| 100 | + class CompletionFinder(argcomplete.CompletionFinder): |
| 101 | + """Hijack the functor from argcomplete to call AutoCompleter""" |
| 102 | + |
| 103 | + def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit, output_stream=None, |
| 104 | + exclude=None, validator=None, print_suppressed=False, append_space=None, |
| 105 | + default_completer=argcomplete.FilesCompleter()): |
| 106 | + """ |
| 107 | + :param argument_parser: The argument parser to autocomplete on |
| 108 | + :type argument_parser: :class:`argparse.ArgumentParser` |
| 109 | + :param always_complete_options: |
| 110 | + Controls the autocompletion of option strings if an option string opening character (normally ``-``) has not |
| 111 | + been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be |
| 112 | + suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short options |
| 113 | + with no long variant will be suggested. If ``short``, short options and long options with no short variant |
| 114 | + will be suggested. |
| 115 | + :type always_complete_options: boolean or string |
| 116 | + :param exit_method: |
| 117 | + Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to |
| 118 | + perform a normal exit that calls exit handlers, use :meth:`sys.exit`. |
| 119 | + :type exit_method: callable |
| 120 | + :param exclude: List of strings representing options to be omitted from autocompletion |
| 121 | + :type exclude: iterable |
| 122 | + :param validator: |
| 123 | + Function to filter all completions through before returning (called with two string arguments, completion |
| 124 | + and prefix; return value is evaluated as a boolean) |
| 125 | + :type validator: callable |
| 126 | + :param print_suppressed: |
| 127 | + Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set. |
| 128 | + :type print_suppressed: boolean |
| 129 | + :param append_space: |
| 130 | + Whether to append a space to unique matches. The default is ``True``. |
| 131 | + :type append_space: boolean |
| 132 | +
|
| 133 | + .. note:: |
| 134 | + If you are not subclassing CompletionFinder to override its behaviors, |
| 135 | + use ``argcomplete.autocomplete()`` directly. It has the same signature as this method. |
| 136 | +
|
| 137 | + Produces tab completions for ``argument_parser``. See module docs for more info. |
| 138 | +
|
| 139 | + Argcomplete only executes actions if their class is known not to have side effects. Custom action classes can be |
| 140 | + added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer argument, or |
| 141 | + their execution is otherwise desirable. |
| 142 | + """ |
| 143 | + self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude, |
| 144 | + validator=validator, print_suppressed=print_suppressed, append_space=append_space, |
| 145 | + default_completer=default_completer) |
| 146 | + |
| 147 | + if "_ARGCOMPLETE" not in os.environ: |
| 148 | + # not an argument completion invocation |
| 149 | + return |
| 150 | + |
| 151 | + try: |
| 152 | + argcomplete.debug_stream = os.fdopen(9, "w") |
| 153 | + except IOError: |
| 154 | + argcomplete.debug_stream = sys.stderr |
| 155 | + |
| 156 | + if output_stream is None: |
| 157 | + try: |
| 158 | + output_stream = os.fdopen(8, "wb") |
| 159 | + except IOError: |
| 160 | + argcomplete.debug("Unable to open fd 8 for writing, quitting") |
| 161 | + exit_method(1) |
| 162 | + |
| 163 | + # print("", stream=debug_stream) |
| 164 | + # for v in "COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY _ARGCOMPLETE_COMP_WORDBREAKS COMP_WORDS".split(): |
| 165 | + # print(v, os.environ[v], stream=debug_stream) |
| 166 | + |
| 167 | + ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013") |
| 168 | + if len(ifs) != 1: |
| 169 | + argcomplete.debug("Invalid value for IFS, quitting [{v}]".format(v=ifs)) |
| 170 | + exit_method(1) |
| 171 | + |
| 172 | + comp_line = os.environ["COMP_LINE"] |
| 173 | + comp_point = int(os.environ["COMP_POINT"]) |
| 174 | + |
| 175 | + comp_line = argcomplete.ensure_str(comp_line) |
| 176 | + |
| 177 | + ############################## |
| 178 | + # SWAPPED FOR AUTOCOMPLETER |
| 179 | + # |
| 180 | + # Replaced with our own tokenizer function |
| 181 | + ############################## |
| 182 | + |
| 183 | + # cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point) |
| 184 | + tokens, _, begidx, endidx = tokens_for_completion(comp_line, comp_point) |
| 185 | + |
| 186 | + # _ARGCOMPLETE is set by the shell script to tell us where comp_words |
| 187 | + # should start, based on what we're completing. |
| 188 | + # 1: <script> [args] |
| 189 | + # 2: python <script> [args] |
| 190 | + # 3: python -m <module> [args] |
| 191 | + start = int(os.environ["_ARGCOMPLETE"]) - 1 |
| 192 | + ############################## |
| 193 | + # SWAPPED FOR AUTOCOMPLETER |
| 194 | + # |
| 195 | + # Applying the same token dropping to our tokens |
| 196 | + ############################## |
| 197 | + # comp_words = comp_words[start:] |
| 198 | + tokens = tokens[start:] |
| 199 | + |
| 200 | + # debug("\nLINE: {!r}".format(comp_line), |
| 201 | + # "\nPOINT: {!r}".format(comp_point), |
| 202 | + # "\nPREQUOTE: {!r}".format(cword_prequote), |
| 203 | + # "\nPREFIX: {!r}".format(cword_prefix), |
| 204 | + # "\nSUFFIX: {!r}".format(cword_suffix), |
| 205 | + # "\nWORDS:", comp_words) |
| 206 | + |
| 207 | + ############################## |
| 208 | + # SWAPPED FOR AUTOCOMPLETER |
| 209 | + # |
| 210 | + # Replaced with our own completion function and customizing the returned values |
| 211 | + ############################## |
| 212 | + # completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos) |
| 213 | + |
| 214 | + # capture stdout from the autocompleter |
| 215 | + result = StringIO() |
| 216 | + with redirect_stdout(result): |
| 217 | + completions = completer.complete_command(tokens, tokens[-1], comp_line, begidx, endidx) |
| 218 | + outstr = result.getvalue() |
| 219 | + |
| 220 | + if completions: |
| 221 | + # If any completion has a space in it, then quote all completions |
| 222 | + # this improves the user experience so they don't nede to go back and add a quote |
| 223 | + if ' ' in ''.join(completions): |
| 224 | + completions = ['"{}"'.format(entry) for entry in completions] |
| 225 | + |
| 226 | + argcomplete.debug("\nReturning completions:", completions) |
| 227 | + |
| 228 | + output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding)) |
| 229 | + elif outstr: |
| 230 | + # if there are no completions, but we got something from stdout, try to print help |
| 231 | + |
| 232 | + # trick the bash completion into thinking there are 2 completions that are unlikely |
| 233 | + # to ever match. |
| 234 | + outstr = outstr.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').strip() |
| 235 | + # generate a filler entry that should always sort first |
| 236 | + filler = ' {0:><{width}}'.format('', width=len(outstr)/2) |
| 237 | + outstr = ifs.join([filler, outstr]) |
| 238 | + |
| 239 | + output_stream.write(outstr.encode(argcomplete.sys_encoding)) |
| 240 | + else: |
| 241 | + # if completions is None we assume we don't know how to handle it so let bash |
| 242 | + # go forward with normal filesystem completion |
| 243 | + output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding)) |
| 244 | + output_stream.flush() |
| 245 | + argcomplete.debug_stream.flush() |
| 246 | + exit_method(0) |
0 commit comments