summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/argcomplete_bridge.py459
-rwxr-xr-xexamples/subcommands.py29
2 files changed, 251 insertions, 237 deletions
diff --git a/cmd2/argcomplete_bridge.py b/cmd2/argcomplete_bridge.py
index f1ed910e..46950cc6 100644
--- a/cmd2/argcomplete_bridge.py
+++ b/cmd2/argcomplete_bridge.py
@@ -1,238 +1,247 @@
# coding=utf-8
"""Hijack the ArgComplete's bash completion handler to return AutoCompleter results"""
-import argcomplete
-from contextlib import redirect_stdout
-import copy
-from io import StringIO
-import os
-import shlex
-import sys
-
-from . import strip_quotes, QUOTES
-
-
-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(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 = [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()):
+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 strip_quotes, QUOTES
+
+
+ def tokens_for_completion(line, endidx):
"""
- :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.
+ 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
"""
- 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)
+ unclosed_quote = ''
+ quotes_to_try = copy.copy(QUOTES)
- if "_ARGCOMPLETE" not in os.environ:
- # not an argument completion invocation
- return
+ tmp_line = line[:endidx]
+ tmp_endidx = endidx
- global debug_stream
- try:
- debug_stream = os.fdopen(9, "w")
- except:
- debug_stream = sys.stderr
+ # 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)
- if output_stream is None:
+ # 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 = [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
+
+ global debug_stream
try:
- output_stream = os.fdopen(8, "wb")
+ debug_stream = os.fdopen(9, "w")
except:
- argcomplete.debug("Unable to open fd 8 for writing, quitting")
+ debug_stream = sys.stderr
+
+ if output_stream is None:
+ try:
+ output_stream = os.fdopen(8, "wb")
+ except:
+ 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)
- # 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))
- 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()
- debug_stream.flush()
- exit_method(0)
+ 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))
+ 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()
+ debug_stream.flush()
+ exit_method(0)
diff --git a/examples/subcommands.py b/examples/subcommands.py
index 01f5b218..9bf6c666 100755
--- a/examples/subcommands.py
+++ b/examples/subcommands.py
@@ -7,8 +7,6 @@
This example shows an easy way for a single command to have many subcommands, each of which takes different arguments
and provides separate contextual help.
"""
-from cmd2.argcomplete_bridge import CompletionFinder
-from cmd2.argparse_completer import AutoCompleter
import argparse
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
@@ -37,16 +35,22 @@ parser_sport = base_subparsers.add_parser('sport', help='sport help')
sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport')
setattr(sport_arg, 'arg_choices', sport_item_strs)
-# Set both a function and tab completer for the "sport" subcommand
-
-if __name__ == '__main__':
- with open('out.txt', 'a') as f:
- f.write('Here 1')
- f.flush()
- completer = CompletionFinder()
- completer(base_parser, AutoCompleter(base_parser))
-
-
+# Handle bash completion if it's installed
+try:
+ # only move forward if we can import CompletionFinder and AutoCompleter
+ from cmd2.argcomplete_bridge import CompletionFinder
+ from cmd2.argparse_completer import AutoCompleter
+ if __name__ == '__main__':
+ with open('out.txt', 'a') as f:
+ f.write('Here 1')
+ f.flush()
+ completer = CompletionFinder()
+ completer(base_parser, AutoCompleter(base_parser))
+except ImportError:
+ pass
+
+
+# Intentionally below the bash completion code to reduce tab completion lag
from cmd2 import cmd2
from cmd2.cmd2 import with_argparser
@@ -73,6 +77,7 @@ class SubcommandsExample(cmd2.Cmd):
"""sport subcommand of base command"""
self.poutput('Sport is {}'.format(args.sport))
+ # Set handler functions for the subcommands
parser_foo.set_defaults(func=base_foo)
parser_bar.set_defaults(func=base_bar)
parser_sport.set_defaults(func=base_sport)