summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2019-02-23 13:38:56 -0500
committerKevin Van Brunt <kmvanbrunt@gmail.com>2019-02-23 13:38:56 -0500
commit9726e982d6c81619e9355db2d495e9dd1a01578d (patch)
treedb91a3ba71bbb483c1dac16d7859cb76ce7ba582 /cmd2
parent7b1b8b10e35b57a813369c9b23876d3615213026 (diff)
downloadcmd2-git-9726e982d6c81619e9355db2d495e9dd1a01578d.tar.gz
Made cmd2_app a positional and required argument of AutoCompleter.
Deleted bash tab completion support. AutoCompleter no longer assumes CompletionItem results are sorted.
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/argcomplete_bridge.py265
-rw-r--r--cmd2/argparse_completer.py54
-rw-r--r--cmd2/cmd2.py6
3 files changed, 21 insertions, 304 deletions
diff --git a/cmd2/argcomplete_bridge.py b/cmd2/argcomplete_bridge.py
deleted file mode 100644
index 885cea31..00000000
--- a/cmd2/argcomplete_bridge.py
+++ /dev/null
@@ -1,265 +0,0 @@
-# coding=utf-8
-"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results"""
-
-try:
- # check if argcomplete is installed
- import argcomplete
-except ImportError: # pragma: no cover
- # not installed, skip the rest of the file
- DEFAULT_COMPLETER = None
-else:
- # argcomplete is installed
-
- # Newer versions of argcomplete have FilesCompleter at top level, older versions only have it under completers
- try:
- DEFAULT_COMPLETER = argcomplete.FilesCompleter()
- except AttributeError:
- DEFAULT_COMPLETER = argcomplete.completers.FilesCompleter()
-
- from cmd2.argparse_completer import ACTION_ARG_CHOICES, ACTION_SUPPRESS_HINT
- from contextlib import redirect_stdout
- import copy
- from io import StringIO
- import os
- import shlex
- import sys
- from typing import List, Tuple, Union
-
- from . import constants
- from . import utils
-
- def tokens_for_completion(line: str, endidx: int) -> Union[Tuple[List[str], List[str], int, int],
- Tuple[None, None, None, None]]:
- """
- Used by tab completion functions to get all tokens through the one being completed
- :param line: the current input line with leading whitespace removed
- :param endidx: the ending index of the prefix text
- :return: A 4 item tuple where the items are
- On Success
- tokens: list of unquoted tokens
- this is generally the list needed for tab completion functions
- raw_tokens: list of tokens with any quotes preserved
- this can be used to know if a token was quoted or is missing a closing quote
- begidx: beginning of last token
- endidx: cursor position
-
- Both lists are guaranteed to have at least 1 item
- The last item in both lists is the token being tab completed
-
- On Failure
- All 4 items are None
- """
- unclosed_quote = ''
- quotes_to_try = copy.copy(constants.QUOTES)
-
- tmp_line = line[:endidx]
- tmp_endidx = endidx
-
- # Parse the line into tokens
- while True:
- try:
- # Use non-POSIX parsing to keep the quotes around the tokens
- initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False)
-
- # calculate begidx
- if unclosed_quote:
- begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1]) + 1
- else:
- if tmp_endidx > 0 and tmp_line[tmp_endidx - 1] == ' ':
- begidx = endidx
- else:
- begidx = tmp_line[:tmp_endidx].rfind(initial_tokens[-1])
-
- # If the cursor is at an empty token outside of a quoted string,
- # then that is the token being completed. Add it to the list.
- if not unclosed_quote and begidx == tmp_endidx:
- initial_tokens.append('')
- break
- except ValueError:
- # ValueError can be caused by missing closing quote
- if not quotes_to_try: # pragma: no cover
- # Since we have no more quotes to try, something else
- # is causing the parsing error. Return None since
- # this means the line is malformed.
- return None, None, None, None
-
- # Add a closing quote and try to parse again
- unclosed_quote = quotes_to_try[0]
- quotes_to_try = quotes_to_try[1:]
-
- tmp_line = line[:endidx]
- tmp_line += unclosed_quote
- tmp_endidx = endidx + 1
-
- raw_tokens = initial_tokens
-
- # Save the unquoted tokens
- tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens]
-
- # If the token being completed had an unclosed quote, we need
- # to remove the closing quote that was added in order for it
- # to match what was on the command line.
- if unclosed_quote:
- raw_tokens[-1] = raw_tokens[-1][:-1]
-
- return tokens, raw_tokens, begidx, endidx
-
- class CompletionFinder(argcomplete.CompletionFinder):
- """Hijack the functor from argcomplete to call AutoCompleter"""
-
- def __call__(self, argument_parser, completer=None, always_complete_options=True, exit_method=os._exit,
- output_stream=None, exclude=None, validator=None, print_suppressed=False, append_space=None,
- default_completer=DEFAULT_COMPLETER):
- """
- :param argument_parser: The argument parser to autocomplete on
- :type argument_parser: :class:`argparse.ArgumentParser`
- :param always_complete_options:
- Controls the autocompletion of option strings if an option string opening character (normally ``-``) has
- not been entered. If ``True`` (default), both short (``-x``) and long (``--x``) option strings will be
- suggested. If ``False``, no option strings will be suggested. If ``long``, long options and short
- options with no long variant will be suggested. If ``short``, short options and long options with no
- short variant will be suggested.
- :type always_complete_options: boolean or string
- :param exit_method:
- Method used to stop the program after printing completions. Defaults to :meth:`os._exit`. If you want to
- perform a normal exit that calls exit handlers, use :meth:`sys.exit`.
- :type exit_method: callable
- :param exclude: List of strings representing options to be omitted from autocompletion
- :type exclude: iterable
- :param validator:
- Function to filter all completions through before returning (called with two string arguments,
- completion and prefix; return value is evaluated as a boolean)
- :type validator: callable
- :param print_suppressed:
- Whether or not to autocomplete options that have the ``help=argparse.SUPPRESS`` keyword argument set.
- :type print_suppressed: boolean
- :param append_space:
- Whether to append a space to unique matches. The default is ``True``.
- :type append_space: boolean
-
- .. note::
- If you are not subclassing CompletionFinder to override its behaviors,
- use ``argcomplete.autocomplete()`` directly. It has the same signature as this method.
-
- Produces tab completions for ``argument_parser``. See module docs for more info.
-
- Argcomplete only executes actions if their class is known not to have side effects. Custom action classes
- can be added to argcomplete.safe_actions, if their values are wanted in the ``parsed_args`` completer
- argument, or their execution is otherwise desirable.
- """
- # Older versions of argcomplete have fewer keyword arguments
- if sys.version_info >= (3, 5):
- self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
- validator=validator, print_suppressed=print_suppressed, append_space=append_space,
- default_completer=default_completer)
- else:
- self.__init__(argument_parser, always_complete_options=always_complete_options, exclude=exclude,
- validator=validator, print_suppressed=print_suppressed)
-
- if "_ARGCOMPLETE" not in os.environ:
- # not an argument completion invocation
- return
-
- try:
- argcomplete.debug_stream = os.fdopen(9, "w")
- except IOError:
- argcomplete.debug_stream = sys.stderr
-
- if output_stream is None:
- try:
- output_stream = os.fdopen(8, "wb")
- except IOError:
- argcomplete.debug("Unable to open fd 8 for writing, quitting")
- exit_method(1)
-
- ifs = os.environ.get("_ARGCOMPLETE_IFS", "\013")
- if len(ifs) != 1:
- argcomplete.debug("Invalid value for IFS, quitting [{v}]".format(v=ifs))
- exit_method(1)
-
- comp_line = os.environ["COMP_LINE"]
- comp_point = int(os.environ["COMP_POINT"])
-
- comp_line = argcomplete.ensure_str(comp_line)
-
- ##############################
- # SWAPPED FOR AUTOCOMPLETER
- #
- # Replaced with our own tokenizer function
- ##############################
- tokens, _, begidx, endidx = tokens_for_completion(comp_line, comp_point)
-
- # _ARGCOMPLETE is set by the shell script to tell us where comp_words
- # should start, based on what we're completing.
- # 1: <script> [args]
- # 2: python <script> [args]
- # 3: python -m <module> [args]
- start = int(os.environ["_ARGCOMPLETE"]) - 1
- ##############################
- # SWAPPED FOR AUTOCOMPLETER
- #
- # Applying the same token dropping to our tokens
- ##############################
- # comp_words = comp_words[start:]
- tokens = tokens[start:]
-
- # debug("\nLINE: {!r}".format(comp_line),
- # "\nPOINT: {!r}".format(comp_point),
- # "\nPREQUOTE: {!r}".format(cword_prequote),
- # "\nPREFIX: {!r}".format(cword_prefix),
- # "\nSUFFIX: {!r}".format(cword_suffix),
- # "\nWORDS:", comp_words)
-
- ##############################
- # SWAPPED FOR AUTOCOMPLETER
- #
- # Replaced with our own completion function and customizing the returned values
- ##############################
- # completions = self._get_completions(comp_words, cword_prefix, cword_prequote, last_wordbreak_pos)
-
- # capture stdout from the autocompleter
- result = StringIO()
- with redirect_stdout(result):
- completions = completer.complete_command(tokens, tokens[-1], comp_line, begidx, endidx)
- outstr = result.getvalue()
-
- if completions:
- # If any completion has a space in it, then quote all completions
- # this improves the user experience so they don't nede to go back and add a quote
- if ' ' in ''.join(completions):
- completions = ['"{}"'.format(entry) for entry in completions]
-
- argcomplete.debug("\nReturning completions:", completions)
-
- output_stream.write(ifs.join(completions).encode(argcomplete.sys_encoding))
- elif outstr:
- # if there are no completions, but we got something from stdout, try to print help
- # trick the bash completion into thinking there are 2 completions that are unlikely
- # to ever match.
-
- comp_type = int(os.environ["COMP_TYPE"])
- if comp_type == 63: # type is 63 for second tab press
- print(outstr.rstrip(), file=argcomplete.debug_stream, end='')
-
- if completions is not None:
- output_stream.write(ifs.join([ifs, ' ']).encode(argcomplete.sys_encoding))
- else:
- output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding))
- else:
- # if completions is None we assume we don't know how to handle it so let bash
- # go forward with normal filesystem completion
- output_stream.write(ifs.join([]).encode(argcomplete.sys_encoding))
- output_stream.flush()
- argcomplete.debug_stream.flush()
- exit_method(0)
-
- def bash_complete(action, show_hint: bool = True):
- """Helper function to configure an argparse action to fall back to bash completion.
-
- This function tags a parameter for bash completion, bypassing the autocompleter (for file input).
- """
- def complete_none(*args, **kwargs):
- return None
-
- setattr(action, ACTION_SUPPRESS_HINT, not show_hint)
- setattr(action, ACTION_ARG_CHOICES, (complete_none,))
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 18549e9e..c9293b4e 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -17,7 +17,7 @@ How to supply completion options for each argument:
parser = argparse.ArgumentParser()
parser.add_argument('-o', '--options', dest='options')
choices = {}
- mycompleter = AutoCompleter(parser, completer, 1, choices)
+ mycompleter = AutoCompleter(parser, cmd2_app, completer, 1, choices)
- static list - provide a static list for each argument name
ex:
@@ -261,21 +261,21 @@ class AutoCompleter(object):
def __init__(self,
parser: argparse.ArgumentParser,
+ cmd2_app,
token_start_index: int = 1,
arg_choices: Dict[str, Union[List, Tuple, Callable]] = None,
subcmd_args_lookup: dict = None,
- tab_for_arg_help: bool = True,
- cmd2_app=None) -> None:
+ tab_for_arg_help: bool = True) -> None:
"""
Create an AutoCompleter
:param parser: ArgumentParser instance
+ :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods
:param token_start_index: index of the token to start parsing at
:param arg_choices: dictionary mapping from argparse argument 'dest' name to list of choices
:param subcmd_args_lookup: mapping a sub-command group name to a tuple to fill the child\
AutoCompleter's arg_choices and subcmd_args_lookup parameters
:param tab_for_arg_help: Enable of disable argument help when there's no completion result
- :param cmd2_app: reference to the Cmd2 application. Enables argparse argument completion with class methods
"""
if not subcmd_args_lookup:
subcmd_args_lookup = {}
@@ -283,10 +283,10 @@ class AutoCompleter(object):
else:
forward_arg_choices = False
self._parser = parser
+ self._cmd2_app = cmd2_app
self._arg_choices = arg_choices.copy() if arg_choices is not None else {}
self._token_start_index = token_start_index
self._tab_for_arg_help = tab_for_arg_help
- self._cmd2_app = cmd2_app
self._flags = [] # all flags in this command
self._flags_without_args = [] # all flags that don't take arguments
@@ -328,11 +328,12 @@ class AutoCompleter(object):
subcmd in args_for_action else \
(arg_choices, subcmd_args_lookup) if forward_arg_choices else ({}, {})
subcmd_start = token_start_index + len(self._positional_actions)
- sub_completers[subcmd] = AutoCompleter(action.choices[subcmd], subcmd_start,
+ sub_completers[subcmd] = AutoCompleter(action.choices[subcmd],
+ cmd2_app,
+ token_start_index=subcmd_start,
arg_choices=subcmd_args,
subcmd_args_lookup=subcmd_lookup,
- tab_for_arg_help=tab_for_arg_help,
- cmd2_app=cmd2_app)
+ tab_for_arg_help=tab_for_arg_help)
sub_commands.append(subcmd)
self._positional_completers[action.dest] = sub_completers
self._arg_choices[action.dest] = sub_commands
@@ -558,8 +559,8 @@ class AutoCompleter(object):
# a flag prefix then we'll complete the list of flag options
if not flag_arg.needed and len(tokens[-1]) > 0 and tokens[-1][0] in self._parser.prefix_chars and \
not skip_remaining_flags:
- return AutoCompleter.basic_complete(text, line, begidx, endidx,
- [flag for flag in self._flags if flag not in matched_flags])
+ return self._cmd2_app.basic_complete(text, line, begidx, endidx,
+ [flag for flag in self._flags if flag not in matched_flags])
# we're not at a positional argument, see if we're in a flag argument
elif not current_is_positional:
if flag_action is not None:
@@ -610,7 +611,6 @@ class AutoCompleter(object):
self._cmd2_app.completion_header = header
self._cmd2_app.display_matches = completions_with_desc
- self._cmd2_app.matches_sorted = True
return completions
@@ -625,7 +625,7 @@ class AutoCompleter(object):
if token in completers:
return completers[token].complete_command_help(tokens, text, line, begidx, endidx)
else:
- return self.basic_complete(text, line, begidx, endidx, completers.keys())
+ return self._cmd2_app.basic_complete(text, line, begidx, endidx, completers.keys())
return []
def format_help(self, tokens: List[str]) -> str:
@@ -687,8 +687,8 @@ class AutoCompleter(object):
# assume this is due to an incorrect function signature, return nothing.
return []
else:
- return AutoCompleter.basic_complete(text, line, begidx, endidx,
- self._resolve_choices_for_arg(action, used_values))
+ return self._cmd2_app.basic_complete(text, line, begidx, endidx,
+ self._resolve_choices_for_arg(action, used_values))
return []
@@ -708,12 +708,9 @@ class AutoCompleter(object):
# is the provided argument a callable. If so, call it
if callable(args):
try:
- if self._cmd2_app is not None:
- try:
- args = args(self._cmd2_app)
- except TypeError:
- args = args()
- else:
+ try:
+ args = args(self._cmd2_app)
+ except TypeError:
args = args()
except TypeError:
return []
@@ -781,21 +778,6 @@ class AutoCompleter(object):
# Redraw prompt and input line
rl_force_redisplay()
- # noinspection PyUnusedLocal
- @staticmethod
- def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against: List[str]) -> List[str]:
- """
- Performs tab completion against a list
-
- :param text: the string prefix we are attempting to match (all returned matches must begin with it)
- :param line: the current input line with leading whitespace removed
- :param begidx: the beginning index of the prefix text
- :param endidx: the ending index of the prefix text
- :param match_against: the list being matched against
- :return: a list of possible tab completions
- """
- return [cur_match for cur_match in match_against if cur_match.startswith(text)]
-
###############################################################################
# Unless otherwise noted, everything below this point are copied from Python's
@@ -1142,7 +1124,7 @@ class ACArgumentParser(argparse.ArgumentParser):
# all args after -- are non-options
if arg_string == '--':
arg_string_pattern_parts.append('-')
- for arg_string in arg_strings_iter:
+ for cur_string in arg_strings_iter:
arg_string_pattern_parts.append('A')
# otherwise, add the arg to the arg strings
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 03fc719e..1f981417 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1574,7 +1574,7 @@ class Cmd(cmd.Cmd):
def _autocomplete_default(self, text: str, line: str, begidx: int, endidx: int,
argparser: argparse.ArgumentParser) -> List[str]:
"""Default completion function for argparse commands."""
- completer = AutoCompleter(argparser, cmd2_app=self)
+ completer = AutoCompleter(argparser, self)
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
if not tokens:
@@ -2564,7 +2564,7 @@ class Cmd(cmd.Cmd):
# Check if this is a command with an argparse function
func = self.cmd_func(command)
if func and hasattr(func, 'argparser'):
- completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self)
+ completer = AutoCompleter(getattr(func, 'argparser'), self)
matches = completer.complete_command_help(tokens[cmd_index:], text, line, begidx, endidx)
return matches
@@ -2593,7 +2593,7 @@ class Cmd(cmd.Cmd):
# Getting help for a specific command
func = self.cmd_func(args.command)
if func and hasattr(func, 'argparser'):
- completer = AutoCompleter(getattr(func, 'argparser'), cmd2_app=self)
+ completer = AutoCompleter(getattr(func, 'argparser'), self)
tokens = [args.command] + args.subcommand
self.poutput(completer.format_help(tokens))
else: