diff options
Diffstat (limited to 'cmd2/argparse_completer.py')
-rw-r--r-- | cmd2/argparse_completer.py | 264 |
1 files changed, 131 insertions, 133 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 2867a553..44982f28 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -188,7 +188,7 @@ class AutoCompleter(object): def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]: """Complete the command using the argparse metadata and provided argument dictionary""" - if not tokens: + if len(tokens) <= self._token_start_index: return [] # Count which positional argument index we're at now. Loop through all tokens on the command line so far @@ -291,131 +291,131 @@ class AutoCompleter(object): # arguments will be hidden from the list of available flags. Also, arguments with a # defined list of possible values will exclude values that have already been used. - # notes when the last token has been reached + # Notes when the token being completed has been reached is_last_token = False - for idx, token in enumerate(tokens): - is_last_token = idx >= len(tokens) - 1 + # Enumerate over the sliced list + for loop_index, token in enumerate(tokens[self._token_start_index:]): + token_index = loop_index + self._token_start_index + if token_index >= len(tokens) - 1: + is_last_token = True + + # If a remainder action is found, force all future tokens to go to that + if remainder['arg'] is not None: + if remainder['action'] == pos_action: + consume_positional_argument() + continue + elif remainder['action'] == flag_action: + consume_flag_argument() + continue + + current_is_positional = False + # Are we consuming flag arguments? + if not flag_arg.needed: + + if not skip_remaining_flags: + # Special case when each of the following is true: + # - We're not in the middle of consuming flag arguments + # - The current positional argument count has hit the max count + # - The next positional argument is a REMAINDER argument + # Argparse will now treat all future tokens as arguments to the positional including tokens that + # look like flags so the completer should skip any flag related processing once this happens + if (pos_action is not None) and pos_arg.count >= pos_arg.max and \ + next_pos_arg_index < len(self._positional_actions) and \ + self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER: + skip_remaining_flags = True - # Only start at the start token index - if idx >= self._token_start_index: + # Handle '--' which tells argparse all remaining arguments are non-flags + if token == '--' and not skip_remaining_flags: + if is_last_token: + # Exit loop and see if -- can be completed into a flag + break + else: + skip_remaining_flags = True - # If a remainder action is found, force all future tokens to go to that - if remainder['arg'] is not None: - if remainder['action'] == pos_action: + # At this point we're no longer consuming flag arguments. Is the current argument a potential flag? + elif is_potential_flag(token, self._parser) and not skip_remaining_flags: + # reset some tracking values + flag_arg.reset() + + # don't reset positional tracking because flags can be interspersed anywhere between positionals + flag_action = None + + # does the token fully match a known flag? + if token in self._flag_to_action: + flag_action = self._flag_to_action[token] + elif hasattr(self._parser, 'allow_abbrev') and self._parser.allow_abbrev: + candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)] + if len(candidates_flags) == 1: + flag_action = self._flag_to_action[candidates_flags[0]] + + if flag_action is not None: + process_argument_nargs(flag_action, flag_arg) + + # It's possible we already have consumed values for this flag if it was used earlier + # in the command line. Reset them now for this use of the flag. + consumed_arg_values[flag_action.dest] = [] + + # Keep track of what flags have already been used + # Flags with action set to append, append_const, and count can be reused + if not is_last_token and \ + not isinstance(flag_action, argparse._AppendAction) and \ + not isinstance(flag_action, argparse._AppendConstAction) and \ + not isinstance(flag_action, argparse._CountAction): + matched_flags.extend(flag_action.option_strings) + + # current token isn't a potential flag + # - does the last flag accept variable arguments? + # - have we reached the max arg count for the flag? + elif not flag_arg.variable or flag_arg.count >= flag_arg.max: + # previous flag doesn't accept variable arguments, count this as a positional argument + + # reset flag tracking variables + flag_arg.reset() + flag_action = None + current_is_positional = True + + if len(token) > 0 and pos_action is not None and pos_arg.count < pos_arg.max: + # we have positional action match and we haven't reached the max arg count, consume + # the positional argument and move on. consume_positional_argument() - continue - elif remainder['action'] == flag_action: - consume_flag_argument() - continue - - current_is_positional = False - # Are we consuming flag arguments? - if not flag_arg.needed: - - if not skip_remaining_flags: - # Special case when each of the following is true: - # - We're not in the middle of consuming flag arguments - # - The current positional argument count has hit the max count - # - The next positional argument is a REMAINDER argument - # Argparse will now treat all future tokens as arguments to the positional including tokens that - # look like flags so the completer should skip any flag related processing once this happens - if (pos_action is not None) and pos_arg.count >= pos_arg.max and \ - next_pos_arg_index < len(self._positional_actions) and \ - self._positional_actions[next_pos_arg_index].nargs == argparse.REMAINDER: - skip_remaining_flags = True - - # Handle '--' which tells argparse all remaining arguments are non-flags - if token == '--' and not skip_remaining_flags: - if is_last_token: - # Exit loop and see if -- can be completed into a flag - break - else: - skip_remaining_flags = True - - # At this point we're no longer consuming flag arguments. Is the current argument a potential flag? - elif is_potential_flag(token, self._parser) and not skip_remaining_flags: - # reset some tracking values - flag_arg.reset() - - # don't reset positional tracking because flags can be interspersed anywhere between positionals - flag_action = None - - # does the token fully match a known flag? - if token in self._flag_to_action: - flag_action = self._flag_to_action[token] - elif hasattr(self._parser, 'allow_abbrev') and self._parser.allow_abbrev: - candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)] - if len(candidates_flags) == 1: - flag_action = self._flag_to_action[candidates_flags[0]] - - if flag_action is not None: - process_argument_nargs(flag_action, flag_arg) - - # It's possible we already have consumed values for this flag if it was used earlier - # in the command line. Reset them now for this use of the flag. - consumed_arg_values[flag_action.dest] = [] - - # Keep track of what flags have already been used - # Flags with action set to append, append_const, and count can be reused - if not is_last_token and \ - not isinstance(flag_action, argparse._AppendAction) and \ - not isinstance(flag_action, argparse._AppendConstAction) and \ - not isinstance(flag_action, argparse._CountAction): - matched_flags.extend(flag_action.option_strings) - - # current token isn't a potential flag - # - does the last flag accept variable arguments? - # - have we reached the max arg count for the flag? - elif not flag_arg.variable or flag_arg.count >= flag_arg.max: - # previous flag doesn't accept variable arguments, count this as a positional argument - - # reset flag tracking variables - flag_arg.reset() - flag_action = None - current_is_positional = True - - if len(token) > 0 and pos_action is not None and pos_arg.count < pos_arg.max: - # we have positional action match and we haven't reached the max arg count, consume - # the positional argument and move on. + elif pos_action is None or pos_arg.count >= pos_arg.max: + # if we don't have a current positional action or we've reached the max count for the action + # close out the current positional argument state and set up for the next one + pos_index = next_pos_arg_index + next_pos_arg_index += 1 + pos_arg.reset() + pos_action = None + + # are we at a sub-command? If so, forward to the matching completer + if pos_index < len(self._positional_actions): + action = self._positional_actions[pos_index] + pos_name = action.dest + if pos_name in self._positional_completers: + sub_completers = self._positional_completers[pos_name] + if token in sub_completers: + return sub_completers[token].complete_command(tokens, text, line, + begidx, endidx) + pos_action = action + process_argument_nargs(pos_action, pos_arg) consume_positional_argument() - elif pos_action is None or pos_arg.count >= pos_arg.max: - # if we don't have a current positional action or we've reached the max count for the action - # close out the current positional argument state and set up for the next one - pos_index = next_pos_arg_index - next_pos_arg_index += 1 - pos_arg.reset() - pos_action = None - - # are we at a sub-command? If so, forward to the matching completer - if pos_index < len(self._positional_actions): - action = self._positional_actions[pos_index] - pos_name = action.dest - if pos_name in self._positional_completers: - sub_completers = self._positional_completers[pos_name] - if token in sub_completers: - return sub_completers[token].complete_command(tokens, text, line, - begidx, endidx) - pos_action = action - process_argument_nargs(pos_action, pos_arg) - consume_positional_argument() - - elif not is_last_token and pos_arg.max is not None: - pos_action = None - pos_arg.reset() - else: - consume_flag_argument() + elif not is_last_token and pos_arg.max is not None: + pos_action = None + pos_arg.reset() else: consume_flag_argument() - # To allow completion of the final token, we only do the following on preceding tokens - if not is_last_token: - if remainder['arg'] is not None: - skip_remaining_flags = True - elif flag_arg.min is not None: - flag_arg.needed = flag_arg.count < flag_arg.min + else: + consume_flag_argument() + + # To allow completion of the final token, we only do the following on preceding tokens + if not is_last_token: + if remainder['arg'] is not None: + skip_remaining_flags = True + elif flag_arg.min is not None: + flag_arg.needed = flag_arg.count < flag_arg.min # Here we're done parsing all of the prior arguments. We know what the next argument is. @@ -509,16 +509,15 @@ class AutoCompleter(object): :param endidx: the ending index of the prefix text :return: List of subcommand completions """ - for idx, token in enumerate(tokens): - if idx >= self._token_start_index: - if self._positional_completers: - # For now argparse only allows 1 sub-command group per level - # so this will only loop once. - for completers in self._positional_completers.values(): - if token in completers: - return completers[token].complete_command_help(tokens, text, line, begidx, endidx) - else: - return utils.basic_complete(text, line, begidx, endidx, completers.keys()) + for token in tokens[self._token_start_index:]: + if self._positional_completers: + # For now argparse only allows 1 sub-command group per level + # so this will only loop once. + for completers in self._positional_completers.values(): + if token in completers: + return completers[token].complete_command_help(tokens, text, line, begidx, endidx) + else: + return utils.basic_complete(text, line, begidx, endidx, completers.keys()) return [] def format_help(self, tokens: List[str]) -> str: @@ -527,14 +526,13 @@ class AutoCompleter(object): :param tokens: command line tokens :return: help text of the subcommand being queried """ - for idx, token in enumerate(tokens): - if idx >= self._token_start_index: - if self._positional_completers: - # For now argparse only allows 1 sub-command group per level - # so this will only loop once. - for completers in self._positional_completers.values(): - if token in completers: - return completers[token].format_help(tokens) + for token in tokens[self._token_start_index:]: + if self._positional_completers: + # For now argparse only allows 1 sub-command group per level + # so this will only loop once. + for completers in self._positional_completers.values(): + if token in completers: + return completers[token].format_help(tokens) return self._parser.format_help() def _complete_for_arg(self, arg: argparse.Action, |