summaryrefslogtreecommitdiff
path: root/cmd2
diff options
context:
space:
mode:
authorKevin Van Brunt <kmvanbrunt@gmail.com>2020-09-04 01:02:53 -0400
committerKevin Van Brunt <kmvanbrunt@gmail.com>2020-09-05 02:14:40 -0400
commite6a9a1c972e5e6195278966c6b134a2f450465e1 (patch)
tree30e1f1101fe64f556ba4a10c5b3e30b9c2e9cf0a /cmd2
parent1054dda76b87d9f7f77311d8d804c1017b668996 (diff)
downloadcmd2-git-e6a9a1c972e5e6195278966c6b134a2f450465e1.tar.gz
Added always_show_hint setting
Fixed issue where flag names weren't always sorted correctly in argparse tab completion
Diffstat (limited to 'cmd2')
-rw-r--r--cmd2/argparse_completer.py68
-rw-r--r--cmd2/cmd2.py49
2 files changed, 73 insertions, 44 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py
index 0efaebe9..04e18d4a 100644
--- a/cmd2/argparse_completer.py
+++ b/cmd2/argparse_completer.py
@@ -35,6 +35,22 @@ DEFAULT_DESCRIPTIVE_HEADER = 'Description'
ARG_TOKENS = 'arg_tokens'
+# noinspection PyProtectedMember
+def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str:
+ """Build tab completion hint for a given argument"""
+ # Check if hinting is disabled for this argument
+ suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False)
+ if suppress_hint or arg_action.help == argparse.SUPPRESS:
+ return ''
+ else:
+ # Use the parser's help formatter to display just this action's help text
+ formatter = parser._get_formatter()
+ formatter.start_section("Hint")
+ formatter.add_argument(arg_action)
+ formatter.end_section()
+ return formatter.format_help()
+
+
def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool:
"""Returns if a token is just a single flag prefix character"""
return len(token) == 1 and token[0] in parser.prefix_chars
@@ -115,7 +131,6 @@ class _UnfinishedFlagError(CompletionError):
super().__init__(error)
-# noinspection PyProtectedMember
class _NoResultsError(CompletionError):
def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
"""
@@ -124,19 +139,8 @@ class _NoResultsError(CompletionError):
:param parser: ArgumentParser instance which owns the action being tab completed
:param arg_action: action being tab completed
"""
- # Check if hinting is disabled
- suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False)
- if suppress_hint or arg_action.help == argparse.SUPPRESS:
- hint_str = ''
- else:
- # Use the parser's help formatter to print just this action's help text
- formatter = parser._get_formatter()
- formatter.start_section("Hint")
- formatter.add_argument(arg_action)
- formatter.end_section()
- hint_str = formatter.format_help()
# Set apply_style to False because we don't want hints to look like errors
- super().__init__(hint_str, apply_style=False)
+ super().__init__(_build_hint(parser, arg_action), apply_style=False)
# noinspection PyProtectedMember
@@ -411,6 +415,7 @@ class ArgparseCompleter:
# If we have results, then return them
if completion_results:
+ self._cmd2_app.completion_hint = _build_hint(self._parser, flag_arg_state.action)
return completion_results
# Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag
@@ -432,6 +437,7 @@ class ArgparseCompleter:
# If we have results, then return them
if completion_results:
+ self._cmd2_app.completion_hint = _build_hint(self._parser, pos_arg_state.action)
return completion_results
# Otherwise, print a hint if text isn't possibly the start of a flag
@@ -566,12 +572,21 @@ class ArgparseCompleter:
"""
# Check if the arg provides choices to the user
if arg_state.action.choices is not None:
- arg_choices = arg_state.action.choices
+ arg_choices = list(arg_state.action.choices)
+ if not arg_choices:
+ return []
+
+ # If these choices are numbers, then sort them now
+ if all(isinstance(x, numbers.Number) for x in arg_choices):
+ arg_choices.sort()
+ self._cmd2_app.matches_sorted = True
+
+ # Since choices can be various types, convert them all to strings
+ arg_choices = [str(x) for x in arg_choices]
else:
arg_choices = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None)
-
- if arg_choices is None:
- return []
+ if arg_choices is None:
+ return []
# If we are going to call a completer/choices function, then set up the common arguments
args = []
@@ -612,19 +627,6 @@ class ArgparseCompleter:
if isinstance(arg_choices, ChoicesCallable) and not arg_choices.is_completer:
arg_choices = arg_choices.to_call(*args, **kwargs)
- # Since arg_choices can be any iterable type, convert to a list
- arg_choices = list(arg_choices)
-
- # If these choices are numbers, and have not yet been sorted, then sort them now
- if not self._cmd2_app.matches_sorted and all(isinstance(x, numbers.Number) for x in arg_choices):
- arg_choices.sort()
- self._cmd2_app.matches_sorted = True
-
- # Since choices can be various types like int, we must convert them to strings
- for index, choice in enumerate(arg_choices):
- if not isinstance(choice, str):
- arg_choices[index] = str(choice)
-
# Filter out arguments we already used
used_values = consumed_arg_values.get(arg_state.action.dest, [])
arg_choices = [choice for choice in arg_choices if choice not in used_values]
@@ -632,4 +634,10 @@ class ArgparseCompleter:
# Do tab completion on the choices
results = basic_complete(text, line, begidx, endidx, arg_choices)
+ if not results:
+ # Reset the value for matches_sorted. This is because completion of flag names
+ # may still be attempted after we return and they haven't been sorted yet.
+ self._cmd2_app.matches_sorted = False
+ return []
+
return self._format_completions(arg_state, results)
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index f3a2d88d..cde3892b 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -208,6 +208,7 @@ class Cmd(cmd.Cmd):
self.allow_redirection = allow_redirection # Security setting to prevent redirection of stdout
# Attributes which ARE dynamically settable via the set command at runtime
+ self.always_show_hint = False
self.debug = False
self.echo = False
self.editor = Cmd.DEFAULT_EDITOR
@@ -375,17 +376,21 @@ class Cmd(cmd.Cmd):
# will be added if there is an unmatched opening quote
self.allow_closing_quote = True
- # An optional header that prints above the tab completion suggestions
+ # An optional hint which prints above tab completion suggestions
+ self.completion_hint = ''
+
+ # Header which prints above CompletionItem tables
self.completion_header = ''
# Used by complete() for readline tab completion
self.completion_matches = []
- # 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.
+ # Use this list if you need to display tab completion suggestions that are different than the actual text
+ # of the matches. For instance, 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.
self.display_matches = []
# Used by functions like path_complete() and delimiter_complete() to properly
@@ -788,6 +793,8 @@ class Cmd(cmd.Cmd):
ansi.STYLE_NEVER),
choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER]))
+ self.add_settable(Settable('always_show_hint', bool,
+ 'Display tab completion hint even when completion suggestions print'))
self.add_settable(Settable('debug', bool, "Show full traceback on exception"))
self.add_settable(Settable('echo', bool, "Echo command issued into output"))
self.add_settable(Settable('editor', str, "Program used by 'edit'"))
@@ -984,6 +991,7 @@ class Cmd(cmd.Cmd):
"""
self.allow_appended_space = True
self.allow_closing_quote = True
+ self.completion_hint = ''
self.completion_header = ''
self.completion_matches = []
self.display_matches = []
@@ -1479,6 +1487,22 @@ class Cmd(cmd.Cmd):
return [cur_match + padding for cur_match in matches_to_display], len(padding)
+ def _build_completion_metadata_string(self) -> str: # pragma: no cover
+ """Build completion metadata string which can contain a hint and CompletionItem table header"""
+ metadata = ''
+
+ # Add hint if one exists and we are supposed to display it
+ if self.always_show_hint and self.completion_hint:
+ metadata += '\n' + self.completion_hint
+
+ # Add table header if one exists
+ if self.completion_header:
+ if not metadata:
+ metadata += '\n'
+ metadata += '\n' + self.completion_header
+
+ return metadata
+
def _display_matches_gnu_readline(self, substitution: str, matches: List[str],
longest_match_length: int) -> None: # pragma: no cover
"""Prints a match list using GNU readline's rl_display_match_list()
@@ -1523,9 +1547,8 @@ 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\n' + self.completion_header)
+ # Print any metadata like a hint or table header
+ sys.stdout.write(self._build_completion_metadata_string())
# Call readline's display function
# rl_display_match_list(strings_array, number of completion matches, longest match length)
@@ -1551,10 +1574,8 @@ 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:
- # noinspection PyUnresolvedReferences
- readline.rl.mode.console.write('\n\n' + self.completion_header)
+ # Print any metadata like a hint or table header
+ readline.rl.mode.console.write(sys.stdout.write(self._build_completion_metadata_string()))
# Display matches using actual display function. This also redraws the prompt and line.
orig_pyreadline_display(matches_to_display)
@@ -3317,7 +3338,7 @@ class Cmd(cmd.Cmd):
# Create the parser for the set command
set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent])
set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable',
- completer_method=complete_set_value)
+ completer_method=complete_set_value, suppress_tab_hint=True)
# Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value
@with_argparser(set_parser, preserve_quotes=True)