Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0a1c41c
Initial approach to the pyscript revamp.
anselor Apr 24, 2018
4193ef0
Initial customization of CompletionFinder
anselor Apr 24, 2018
e018bbd
Removed the expensive imports from cmd2/__init__.py
anselor Apr 25, 2018
ae86103
Added checks to detect if argcomplete is installed.
anselor Apr 25, 2018
2a68a0e
Trap SystemExit when calling argparse.parse() to on argparse commands.
anselor Apr 25, 2018
9351245
small tweak. saving state.
anselor Apr 27, 2018
54f9a7a
Merge remote-tracking branch 'origin/master' into pyscript
anselor Apr 28, 2018
452c396
Merge remote-tracking branch 'origin/master' into bash_completion
anselor Apr 28, 2018
8d8db3f
Merge branch 'master' into bash_completion
tleonhardt Apr 28, 2018
ab7ac49
Added support for translating function positional and keyword argumen…
anselor Apr 28, 2018
e5699bc
Added more tests exercising the pyscript bridge.
anselor Apr 30, 2018
4a36b8c
Further customization of argparse applying patch submitted to https:/…
anselor Apr 30, 2018
839d957
Added support for different argument modes and tests to validate.
anselor May 1, 2018
bf52888
Added CommandResult which returns stdout, stderr, and command data
anselor May 1, 2018
2528fb5
Added support for customizing the pyscript bridge pystate object name.
anselor May 2, 2018
97668f9
Restored legacy cmd/self access when locals_in_py is True. Changed de…
anselor May 2, 2018
d2d3c2f
Updated some documentation.
anselor May 2, 2018
148f05d
Addressed comments.
anselor May 2, 2018
a95c8a0
Merge branch 'bash_completion' into bash_to_pyscript
anselor May 2, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* See the [tab_autocompletion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocompletion.py) example for a demonstration of how to use this feature
* ``cmd2`` no longer depends on the ``six`` module
* ``cmd2`` is now a multi-file Python package instead of a single-file module
* New pyscript approach that provides a pythonic interface to commands in the cmd2 application.
* Deletions (potentially breaking changes)
* Deleted all ``optparse`` code which had previously been deprecated in release 0.8.0
* The ``options`` decorator no longer exists
Expand All @@ -12,6 +13,7 @@
* Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py)
* Deleted ``cmd_with_subs_completer``, ``get_subcommands``, and ``get_subcommand_completer``
* Replaced by default AutoCompleter implementation for all commands using argparse
* Deleted support for old method of calling application commands with ``cmd()`` and ``self``
* Python 2 no longer supported
* ``cmd2`` now supports Python 3.4+

Expand Down
3 changes: 0 additions & 3 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
#
# -*- coding: utf-8 -*-
#
from .cmd2 import __version__, Cmd, set_posix_shlex, set_strip_quotes, AddSubmenu, CmdResult, categorize
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
246 changes: 246 additions & 0 deletions cmd2/argcomplete_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
# coding=utf-8
"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results"""

try:
# check if argcomplete is installed
import argcomplete
except ImportError:
# not installed, skip the rest of the file
pass

else:
# argcomplete is installed

from contextlib import redirect_stdout
import copy
from io import StringIO
import os
import shlex
import sys

from . import constants
from . import utils


def tokens_for_completion(line, endidx):
"""
Used by tab completion functions to get all tokens through the one being completed
:param line: str - the current input line with leading whitespace removed
:param endidx: int - 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
Both 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:
# 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=argcomplete.FilesCompleter()):
"""
: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.
"""
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)

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)

# print("", stream=debug_stream)
# for v in "COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY _ARGCOMPLETE_COMP_WORDBREAKS COMP_WORDS".split():
# print(v, os.environ[v], stream=debug_stream)

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
##############################

# cword_prequote, cword_prefix, cword_suffix, comp_words, last_wordbreak_pos = split_line(comp_line, comp_point)
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.
outstr = outstr.replace('\n', ' ').replace('\t', ' ').replace(' ', ' ').strip()
# generate a filler entry that should always sort first
filler = ' {0:><{width}}'.format('', width=len(outstr)/2)
outstr = ifs.join([filler, outstr])

output_stream.write(outstr.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)
Loading