Skip to content

cmd2.argparse_completer

cmd2.argparse_completer

This module defines the ArgparseCompleter class which provides argparse-based tab completion to cmd2 apps. See the header of argparse_custom.py for instructions on how to use these features.

DEFAULT_DESCRIPTIVE_HEADER module-attribute

DEFAULT_DESCRIPTIVE_HEADER = 'Description'

ARG_TOKENS module-attribute

ARG_TOKENS = 'arg_tokens'

DEFAULT_AP_COMPLETER module-attribute

DEFAULT_AP_COMPLETER = ArgparseCompleter

ArgparseCompleter

ArgparseCompleter(parser, cmd2_app, *, parent_tokens=None)

Automatic command line tab completion based on argparse parameters

Create an ArgparseCompleter

PARAMETER DESCRIPTION
parser

ArgumentParser instance

TYPE: ArgumentParser

cmd2_app

reference to the Cmd2 application that owns this ArgparseCompleter

TYPE: Cmd

parent_tokens

optional dictionary mapping parent parsers' arg names to their tokens This is only used by ArgparseCompleter when recursing on subcommand parsers Defaults to None

TYPE: Optional[Dict[str, List[str]]] DEFAULT: None

Source code in cmd2/argparse_completer.py
def __init__(
    self, parser: argparse.ArgumentParser, cmd2_app: 'Cmd', *, parent_tokens: Optional[Dict[str, List[str]]] = None
) -> None:
    """
    Create an ArgparseCompleter

    :param parser: ArgumentParser instance
    :param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter
    :param parent_tokens: optional dictionary mapping parent parsers' arg names to their tokens
                          This is only used by ArgparseCompleter when recursing on subcommand parsers
                          Defaults to None
    """
    self._parser = parser
    self._cmd2_app = cmd2_app

    if parent_tokens is None:
        parent_tokens = dict()
    self._parent_tokens = parent_tokens

    self._flags = []  # all flags in this command
    self._flag_to_action = {}  # maps flags to the argparse action object
    self._positional_actions = []  # actions for positional arguments (by position index)
    self._subcommand_action = None  # this will be set if self._parser has subcommands

    # Start digging through the argparse structures.
    # _actions is the top level container of parameter definitions
    for action in self._parser._actions:
        # if the parameter is flag based, it will have option_strings
        if action.option_strings:
            # record each option flag
            for option in action.option_strings:
                self._flags.append(option)
                self._flag_to_action[option] = action

        # Otherwise this is a positional parameter
        else:
            self._positional_actions.append(action)
            # Check if this action defines subcommands
            if isinstance(action, argparse._SubParsersAction):
                self._subcommand_action = action

complete

complete(text, line, begidx, endidx, tokens, *, cmd_set=None)

Complete text using argparse metadata

PARAMETER DESCRIPTION
text

the string prefix we are attempting to match (all matches must begin with it)

TYPE: str

line

the current input line with leading whitespace removed

TYPE: str

begidx

the beginning index of the prefix text

TYPE: int

endidx

the ending index of the prefix text

TYPE: int

tokens

list of argument tokens being passed to the parser

TYPE: List[str]

cmd_set

if tab completing a command, the CommandSet the command's function belongs to, if applicable. Defaults to None.

TYPE: Optional[CommandSet] DEFAULT: None

RAISES DESCRIPTION
CompletionError

for various types of tab completion errors

Source code in cmd2/argparse_completer.py
def complete(
    self, text: str, line: str, begidx: int, endidx: int, tokens: List[str], *, cmd_set: Optional[CommandSet] = None
) -> List[str]:
    """
    Complete text using argparse metadata

    :param text: the string prefix we are attempting to match (all matches must begin with it)
    :param line: the current input line with leading whitespace removed
    :param begidx: the beginning index of the prefix text
    :param endidx: the ending index of the prefix text
    :param tokens: list of argument tokens being passed to the parser
    :param cmd_set: if tab completing a command, the CommandSet the command's function belongs to, if applicable.
                    Defaults to None.

    :raises CompletionError: for various types of tab completion errors
    """
    if not tokens:
        return []

    # Positionals args that are left to parse
    remaining_positionals = deque(self._positional_actions)

    # This gets set to True when flags will no longer be processed as argparse flags
    # That can happen when -- is used or an argument with nargs=argparse.REMAINDER is used
    skip_remaining_flags = False

    # _ArgumentState of the current positional
    pos_arg_state: Optional[_ArgumentState] = None

    # _ArgumentState of the current flag
    flag_arg_state: Optional[_ArgumentState] = None

    # Non-reusable flags that we've parsed
    matched_flags: List[str] = []

    # Keeps track of arguments we've seen and any tokens they consumed
    consumed_arg_values: Dict[str, List[str]] = dict()  # dict(arg_name -> List[tokens])

    # Completed mutually exclusive groups
    completed_mutex_groups: Dict[argparse._MutuallyExclusiveGroup, argparse.Action] = dict()

    def consume_argument(arg_state: _ArgumentState) -> None:
        """Consuming token as an argument"""
        arg_state.count += 1
        consumed_arg_values.setdefault(arg_state.action.dest, [])
        consumed_arg_values[arg_state.action.dest].append(token)

    def update_mutex_groups(arg_action: argparse.Action) -> None:
        """
        Check if an argument belongs to a mutually exclusive group and either mark that group
        as complete or print an error if the group has already been completed
        :param arg_action: the action of the argument
        :raises CompletionError: if the group is already completed
        """
        # Check if this action is in a mutually exclusive group
        for group in self._parser._mutually_exclusive_groups:
            if arg_action in group._group_actions:
                # Check if the group this action belongs to has already been completed
                if group in completed_mutex_groups:
                    # If this is the action that completed the group, then there is no error
                    # since it's allowed to appear on the command line more than once.
                    completer_action = completed_mutex_groups[group]
                    if arg_action == completer_action:
                        return

                    error = "Error: argument {}: not allowed with argument {}".format(
                        argparse._get_action_name(arg_action), argparse._get_action_name(completer_action)
                    )
                    raise CompletionError(error)

                # Mark that this action completed the group
                completed_mutex_groups[group] = arg_action

                # Don't tab complete any of the other args in the group
                for group_action in group._group_actions:
                    if group_action == arg_action:
                        continue
                    elif group_action in self._flag_to_action.values():
                        matched_flags.extend(group_action.option_strings)
                    elif group_action in remaining_positionals:
                        remaining_positionals.remove(group_action)

                # Arg can only be in one group, so we are done
                break

    #############################################################################################
    # Parse all but the last token
    #############################################################################################
    for token_index, token in enumerate(tokens[:-1]):
        # If we're in a positional REMAINDER arg, force all future tokens to go to that
        if pos_arg_state is not None and pos_arg_state.is_remainder:
            consume_argument(pos_arg_state)
            continue

        # If we're in a flag REMAINDER arg, force all future tokens to go to that until a double dash is hit
        elif flag_arg_state is not None and flag_arg_state.is_remainder:
            if token == '--':
                flag_arg_state = None
            else:
                consume_argument(flag_arg_state)
            continue

        # Handle '--' which tells argparse all remaining arguments are non-flags
        elif token == '--' and not skip_remaining_flags:
            # Check if there is an unfinished flag
            if (
                flag_arg_state is not None
                and isinstance(flag_arg_state.min, int)
                and flag_arg_state.count < flag_arg_state.min
            ):
                raise _UnfinishedFlagError(flag_arg_state)

            # Otherwise end the current flag
            else:
                flag_arg_state = None
                skip_remaining_flags = True
                continue

        # Check the format of the current token to see if it can be an argument's value
        if _looks_like_flag(token, self._parser) and not skip_remaining_flags:
            # Check if there is an unfinished flag
            if (
                flag_arg_state is not None
                and isinstance(flag_arg_state.min, int)
                and flag_arg_state.count < flag_arg_state.min
            ):
                raise _UnfinishedFlagError(flag_arg_state)

            # Reset flag arg state but not positional tracking because flags can be
            # interspersed anywhere between positionals
            flag_arg_state = None
            action = None

            # Does the token match a known flag?
            if token in self._flag_to_action:
                action = self._flag_to_action[token]
            elif self._parser.allow_abbrev:
                candidates_flags = [flag for flag in self._flag_to_action if flag.startswith(token)]
                if len(candidates_flags) == 1:
                    action = self._flag_to_action[candidates_flags[0]]

            if action is not None:
                update_mutex_groups(action)
                if isinstance(action, (argparse._AppendAction, argparse._AppendConstAction, argparse._CountAction)):
                    # Flags with action set to append, append_const, and count can be reused
                    # Therefore don't erase any tokens already consumed for this flag
                    consumed_arg_values.setdefault(action.dest, [])
                else:
                    # This flag is not reusable, so mark that we've seen it
                    matched_flags.extend(action.option_strings)

                    # 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 it.
                    consumed_arg_values[action.dest] = []

                new_arg_state = _ArgumentState(action)

                # Keep track of this flag if it can receive arguments
                if new_arg_state.max > 0:  # type: ignore[operator]
                    flag_arg_state = new_arg_state
                    skip_remaining_flags = flag_arg_state.is_remainder

        # Check if we are consuming a flag
        elif flag_arg_state is not None:
            consume_argument(flag_arg_state)

            # Check if we have finished with this flag
            if isinstance(flag_arg_state.max, (float, int)) and flag_arg_state.count >= flag_arg_state.max:
                flag_arg_state = None

        # Otherwise treat as a positional argument
        else:
            # If we aren't current tracking a positional, then get the next positional arg to handle this token
            if pos_arg_state is None:
                # Make sure we are still have positional arguments to parse
                if remaining_positionals:
                    action = remaining_positionals.popleft()

                    # Are we at a subcommand? If so, forward to the matching completer
                    if action == self._subcommand_action:
                        if token in self._subcommand_action.choices:
                            # Merge self._parent_tokens and consumed_arg_values
                            parent_tokens = {**self._parent_tokens, **consumed_arg_values}

                            # Include the subcommand name if its destination was set
                            if action.dest != argparse.SUPPRESS:
                                parent_tokens[action.dest] = [token]

                            parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
                            completer_type = self._cmd2_app._determine_ap_completer_type(parser)

                            completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens)

                            return completer.complete(
                                text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set
                            )
                        else:
                            # Invalid subcommand entered, so no way to complete remaining tokens
                            return []

                    # Otherwise keep track of the argument
                    else:
                        pos_arg_state = _ArgumentState(action)

            # Check if we have a positional to consume this token
            if pos_arg_state is not None:
                update_mutex_groups(pos_arg_state.action)
                consume_argument(pos_arg_state)

                # No more flags are allowed if this is a REMAINDER argument
                if pos_arg_state.is_remainder:
                    skip_remaining_flags = True

                # Check if we have finished with this positional
                elif isinstance(pos_arg_state.max, (float, int)) and pos_arg_state.count >= pos_arg_state.max:
                    pos_arg_state = None

                    # Check if the next positional has nargs set to argparse.REMAINDER.
                    # At this point argparse allows no more flags to be processed.
                    if remaining_positionals and remaining_positionals[0].nargs == argparse.REMAINDER:
                        skip_remaining_flags = True

    #############################################################################################
    # We have parsed all but the last token and have enough information to complete it
    #############################################################################################

    # Check if we are completing a flag name. This check ignores strings with a length of one, like '-'.
    # This is because that could be the start of a negative number which may be a valid completion for
    # the current argument. We will handle the completion of flags that start with only one prefix
    # character (-f) at the end.
    if _looks_like_flag(text, self._parser) and not skip_remaining_flags:
        if (
            flag_arg_state is not None
            and isinstance(flag_arg_state.min, int)
            and flag_arg_state.count < flag_arg_state.min
        ):
            raise _UnfinishedFlagError(flag_arg_state)
        return self._complete_flags(text, line, begidx, endidx, matched_flags)

    completion_results = []

    # Check if we are completing a flag's argument
    if flag_arg_state is not None:
        completion_results = self._complete_arg(
            text, line, begidx, endidx, flag_arg_state, consumed_arg_values, cmd_set=cmd_set
        )

        # If we have results, then return them
        if completion_results:
            # Don't overwrite an existing hint
            if not self._cmd2_app.completion_hint:
                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
        elif (
            (isinstance(flag_arg_state.min, int) and flag_arg_state.count < flag_arg_state.min)
            or not _single_prefix_char(text, self._parser)
            or skip_remaining_flags
        ):
            raise _NoResultsError(self._parser, flag_arg_state.action)

    # Otherwise check if we have a positional to complete
    elif pos_arg_state is not None or remaining_positionals:
        # If we aren't current tracking a positional, then get the next positional arg to handle this token
        if pos_arg_state is None:
            action = remaining_positionals.popleft()
            pos_arg_state = _ArgumentState(action)

        completion_results = self._complete_arg(
            text, line, begidx, endidx, pos_arg_state, consumed_arg_values, cmd_set=cmd_set
        )

        # If we have results, then return them
        if completion_results:
            # Don't overwrite an existing hint
            if not self._cmd2_app.completion_hint:
                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
        elif not _single_prefix_char(text, self._parser) or skip_remaining_flags:
            raise _NoResultsError(self._parser, pos_arg_state.action)

    # If we aren't skipping remaining flags, then complete flag names if either is True:
    #   1. text is a single flag prefix character that didn't complete against any argument values
    #   2. there are no more positionals to complete
    if not skip_remaining_flags and (_single_prefix_char(text, self._parser) or not remaining_positionals):
        # Reset any completion settings that may have been set by functions which actually had no matches.
        # Otherwise, those settings could alter how the flags are displayed.
        self._cmd2_app._reset_completion_defaults()
        return self._complete_flags(text, line, begidx, endidx, matched_flags)

    return completion_results

complete_subcommand_help

complete_subcommand_help(text, line, begidx, endidx, tokens)

Supports cmd2's help command in the completion of subcommand names

PARAMETER DESCRIPTION
text

the string prefix we are attempting to match (all matches must begin with it)

TYPE: str

line

the current input line with leading whitespace removed

TYPE: str

begidx

the beginning index of the prefix text

TYPE: int

endidx

the ending index of the prefix text

TYPE: int

tokens

arguments passed to command/subcommand

TYPE: List[str]

RETURNS DESCRIPTION
List[str]

List of subcommand completions

Source code in cmd2/argparse_completer.py
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:
    """
    Supports cmd2's help command in the completion of subcommand names
    :param text: the string prefix we are attempting to match (all matches must begin with it)
    :param line: the current input line with leading whitespace removed
    :param begidx: the beginning index of the prefix text
    :param endidx: the ending index of the prefix text
    :param tokens: arguments passed to command/subcommand
    :return: List of subcommand completions
    """
    # If our parser has subcommands, we must examine the tokens and check if they are subcommands
    # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
    if self._subcommand_action is not None:
        for token_index, token in enumerate(tokens):
            if token in self._subcommand_action.choices:
                parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
                completer_type = self._cmd2_app._determine_ap_completer_type(parser)

                completer = completer_type(parser, self._cmd2_app)
                return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :])
            elif token_index == len(tokens) - 1:
                # Since this is the last token, we will attempt to complete it
                return self._cmd2_app.basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
            else:
                break
    return []

format_help

format_help(tokens)

Supports cmd2's help command in the retrieval of help text

PARAMETER DESCRIPTION
tokens

arguments passed to help command

TYPE: List[str]

RETURNS DESCRIPTION
str

help text of the command being queried

Source code in cmd2/argparse_completer.py
def format_help(self, tokens: List[str]) -> str:
    """
    Supports cmd2's help command in the retrieval of help text
    :param tokens: arguments passed to help command
    :return: help text of the command being queried
    """
    # If our parser has subcommands, we must examine the tokens and check if they are subcommands
    # If so, we will let the subcommand's parser handle the rest of the tokens via another ArgparseCompleter.
    if self._subcommand_action is not None:
        for token_index, token in enumerate(tokens):
            if token in self._subcommand_action.choices:
                parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
                completer_type = self._cmd2_app._determine_ap_completer_type(parser)

                completer = completer_type(parser, self._cmd2_app)
                return completer.format_help(tokens[token_index + 1 :])
            else:
                break
    return self._parser.format_help()

set_default_ap_completer_type

set_default_ap_completer_type(completer_type)

Set the default ArgparseCompleter class for a cmd2 app.

PARAMETER DESCRIPTION
completer_type

Type that is a subclass of ArgparseCompleter.

TYPE: Type[ArgparseCompleter]

Source code in cmd2/argparse_completer.py
def set_default_ap_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
    """
    Set the default ArgparseCompleter class for a cmd2 app.

    :param completer_type: Type that is a subclass of ArgparseCompleter.
    """
    global DEFAULT_AP_COMPLETER
    DEFAULT_AP_COMPLETER = completer_type