summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-06-07 16:57:30 -0700
committerGitHub <noreply@github.com>2018-06-07 16:57:30 -0700
commiteb8181e7d3f900cecd1455efd75f3b5c5f6918cd (patch)
tree97aa7c7151cc8f5c6923d0d965d650af75d7115e
parentd0e71c85190b81bb269cc18bf9380a142e18d707 (diff)
parente4d13c745b7b983eb444e97f467b572a885d220c (diff)
downloadcmd2-git-eb8181e7d3f900cecd1455efd75f3b5c5f6918cd.tar.gz
Merge pull request #433 from python-cmd2/autocompleter
Autocompleter
-rwxr-xr-xcmd2/argparse_completer.py38
-rw-r--r--cmd2/cmd2.py44
-rwxr-xr-xexamples/tab_autocompletion.py18
3 files changed, 84 insertions, 16 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index d98a6eac..904fb7b3 100755
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -59,6 +59,7 @@ Released under MIT license, see LICENSE file
import argparse
from colorama import Fore
+import os
import sys
from typing import List, Dict, Tuple, Callable, Union
@@ -78,6 +79,15 @@ ACTION_ARG_CHOICES = 'arg_choices'
ACTION_SUPPRESS_HINT = 'suppress_hint'
+class CompletionItem(str):
+ def __new__(cls, o, desc='', *args, **kwargs):
+ return str.__new__(cls, o, *args, **kwargs)
+
+ # noinspection PyMissingConstructor,PyUnusedLocal
+ def __init__(self, o, desc='', *args, **kwargs):
+ self.description = desc
+
+
class _RangeAction(object):
def __init__(self, nargs: Union[int, str, Tuple[int, int], None]):
self.nargs_min = None
@@ -413,6 +423,8 @@ class AutoCompleter(object):
completion_results = self._complete_for_arg(flag_action, text, line, begidx, endidx, consumed)
if not completion_results:
self._print_action_help(flag_action)
+ elif len(completion_results) > 1:
+ completion_results = self._format_completions(flag_action, completion_results)
# ok, we're not a flag, see if there's a positional argument to complete
else:
@@ -422,9 +434,35 @@ class AutoCompleter(object):
completion_results = self._complete_for_arg(pos_action, text, line, begidx, endidx, consumed)
if not completion_results:
self._print_action_help(pos_action)
+ elif len(completion_results) > 1:
+ completion_results = self._format_completions(pos_action, completion_results)
return completion_results
+ def _format_completions(self, action, completions: List[Union[str, CompletionItem]]):
+ if completions and len(completions) > 1 and isinstance(completions[0], CompletionItem):
+ token_width = len(action.dest)
+ completions_with_desc = []
+
+ for item in completions:
+ if len(item) > token_width:
+ token_width = len(item)
+
+ term_size = os.get_terminal_size()
+ fill_width = int(term_size.columns * .6) - (token_width + 2)
+ for item in completions:
+ entry = '{: <{token_width}}{: <{fill_width}}'.format(item, item.description,
+ token_width=token_width+2,
+ fill_width=fill_width)
+ completions_with_desc.append(entry)
+
+ header = '\n{: <{token_width}}{}'.format(action.dest.upper(), action.desc_header, token_width=token_width+2)
+
+ self._cmd2_app.completion_header = header
+ self._cmd2_app.display_matches = completions_with_desc
+
+ return completions
+
def complete_command_help(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
"""Supports the completion of sub-commands for commands through the cmd2 help command."""
for idx, token in enumerate(tokens):
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 289c44ae..bb4ac4ad 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -33,6 +33,7 @@ import argparse
import cmd
import collections
from colorama import Fore
+import copy
import glob
import os
import platform
@@ -494,13 +495,19 @@ class Cmd(cmd.Cmd):
# will be added if there is an unmatched opening quote
self.allow_closing_quote = True
- # Use this list if you are completing strings that contain a common delimiter and you only want to
- # display the final portion of the matches as the tab-completion suggestions. The full matches
- # still must be returned from your completer function. For an example, look at path_complete()
- # which uses this to show only the basename of paths as the suggestions. delimiter_complete() also
- # populates this list.
+ # An optional header that prints above the tab-completion suggestions
+ self.completion_header = ''
+
+ # If the tab-completion suggestions should be displayed in a way that is different than the actual match values,
+ # then place those results in this list. The full matches still must be returned from your completer function.
+ # For an example, look at path_complete() which uses this to show only the basename of paths as the
+ # suggestions. delimiter_complete() also populates this list.
self.display_matches = []
+ # Used by functions like path_complete() and delimiter_complete() to properly
+ # quote matches that are completed in a delimited fashion
+ self.matches_delimited = False
+
# ----- Methods related to presenting output to the user -----
@property
@@ -657,7 +664,9 @@ class Cmd(cmd.Cmd):
"""
self.allow_appended_space = True
self.allow_closing_quote = True
+ self.completion_header = ''
self.display_matches = []
+ self.matches_delimited = False
if rl_type == RlType.GNU:
readline.set_completion_display_matches_hook(self._display_matches_gnu_readline)
@@ -683,7 +692,6 @@ class Cmd(cmd.Cmd):
On Failure
Both items are None
"""
- import copy
unclosed_quote = ''
quotes_to_try = copy.copy(constants.QUOTES)
@@ -836,6 +844,8 @@ class Cmd(cmd.Cmd):
# Display only the portion of the match that's being completed based on delimiter
if matches:
+ # Set this to True for proper quoting of matches with spaces
+ self.matches_delimited = True
# Get the common beginning for the matches
common_prefix = os.path.commonprefix(matches)
@@ -1037,6 +1047,9 @@ class Cmd(cmd.Cmd):
search_str = os.path.join(os.getcwd(), search_str)
cwd_added = True
+ # Set this to True for proper quoting of paths with spaces
+ self.matches_delimited = True
+
# Find all matching path completions
matches = glob.glob(search_str)
@@ -1245,6 +1258,10 @@ class Cmd(cmd.Cmd):
strings_array[1:-1] = encoded_matches
strings_array[-1] = None
+ # Print the header if one exists
+ if self.completion_header:
+ sys.stdout.write('\n' + self.completion_header)
+
# Call readline's display function
# rl_display_match_list(strings_array, number of completion matches, longest match length)
readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length)
@@ -1270,6 +1287,10 @@ class Cmd(cmd.Cmd):
# Add padding for visual appeal
matches_to_display, _ = self._pad_matches_to_display(matches_to_display)
+ # Print the header if one exists
+ if self.completion_header:
+ readline.rl.mode.console.write('\n' + self.completion_header)
+
# Display matches using actual display function. This also redraws the prompt and line.
orig_pyreadline_display(matches_to_display)
@@ -1414,17 +1435,10 @@ class Cmd(cmd.Cmd):
display_matches_set = set(self.display_matches)
self.display_matches = list(display_matches_set)
- # Check if display_matches has been used. If so, then matches
- # on delimited strings like paths was done.
- if self.display_matches:
- matches_delimited = True
- else:
- matches_delimited = False
-
+ if not self.display_matches:
# Since self.display_matches is empty, set it to self.completion_matches
# before we alter them. That way the suggestions will reflect how we parsed
# the token being completed and not how readline did.
- import copy
self.display_matches = copy.copy(self.completion_matches)
# Check if we need to add an opening quote
@@ -1435,7 +1449,7 @@ class Cmd(cmd.Cmd):
# This is the tab completion text that will appear on the command line.
common_prefix = os.path.commonprefix(self.completion_matches)
- if matches_delimited:
+ if self.matches_delimited:
# Check if any portion of the display matches appears in the tab completion
display_prefix = os.path.commonprefix(self.display_matches)
diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py
index d1726841..817ceb72 100755
--- a/examples/tab_autocompletion.py
+++ b/examples/tab_autocompletion.py
@@ -109,6 +109,18 @@ class TabCompleteExample(cmd2.Cmd):
"""Simulating a function that queries and returns a completion values"""
return actors
+ def instance_query_movie_ids(self) -> List[str]:
+ """Demonstrates showing tabular hinting of tab completion information"""
+ completions_with_desc = []
+
+ for movie_id, movie_entry in self.MOVIE_DATABASE.items():
+ completions_with_desc.append(argparse_completer.CompletionItem(movie_id, movie_entry['title']))
+
+ setattr(self.vid_delete_movie_id, 'desc_header', 'Title')
+ setattr(self.movies_delete_movie_id, 'desc_header', 'Title')
+
+ return completions_with_desc
+
# This demonstrates a number of customizations of the AutoCompleter version of ArgumentParser
# - The help output will separately group required vs optional flags
# - The help output for arguments with multiple flags or with append=True is more concise
@@ -253,6 +265,8 @@ class TabCompleteExample(cmd2.Cmd):
('path_complete', [False, False]))
vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete')
+ vid_delete_movie_id = vid_movies_delete_parser.add_argument('movie_id', help='Movie ID')
+ setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, instance_query_movie_ids)
vid_shows_parser = video_types_subparsers.add_parser('shows')
vid_shows_parser.set_defaults(func=_do_vid_media_shows)
@@ -328,6 +342,8 @@ class TabCompleteExample(cmd2.Cmd):
movies_add_parser.add_argument('actor', help='Actors', nargs='*')
movies_delete_parser = movies_commands_subparsers.add_parser('delete')
+ movies_delete_movie_id = movies_delete_parser.add_argument('movie_id', help='Movie ID')
+ setattr(movies_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_movie_ids')
movies_load_parser = movies_commands_subparsers.add_parser('load')
movie_file_action = movies_load_parser.add_argument('movie_file', help='Movie database')
@@ -362,7 +378,7 @@ class TabCompleteExample(cmd2.Cmd):
'director': TabCompleteExample.static_list_directors, # static list
'movie_file': (self.path_complete, [False, False])
}
- completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices)
+ completer = argparse_completer.AutoCompleter(TabCompleteExample.media_parser, arg_choices=choices, cmd2_app=self)
tokens, _ = self.tokens_for_completion(line, begidx, endidx)
results = completer.complete_command(tokens, text, line, begidx, endidx)