Skip to content

Commit a95c8a0

Browse files
committed
Merge branch 'bash_completion' into bash_to_pyscript
2 parents d2d3c2f + 148f05d commit a95c8a0

28 files changed

+395
-140
lines changed

cmd2/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,2 @@
11
#
22
# -*- coding: utf-8 -*-
3-
#
4-
from .cmd2 import __version__, Cmd, set_posix_shlex, set_strip_quotes, AddSubmenu, CmdResult, categorize
5-
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category

cmd2/argcomplete_bridge.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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

Comments
 (0)