Skip to content

cmd2.argparse_completer

cmd2.argparse_completer

Module defines the ArgparseCompleter class which provides argparse-based completion to cmd2 apps.

See the header of argparse_utils.py for instructions on how to use these features.

ARG_TOKENS module-attribute

ARG_TOKENS = 'arg_tokens'

DEFAULT_ARGPARSE_COMPLETER module-attribute

DEFAULT_ARGPARSE_COMPLETER = ArgparseCompleter

ArgparseCompleter

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

Automatic command line completion based on argparse parameters.

Create an ArgparseCompleter.

PARAMETER DESCRIPTION
parser

Cmd2ArgumentParser instance

TYPE: Cmd2ArgumentParser

cmd_app

reference to the cmd2.Cmd instance that owns this ArgparseCompleter

TYPE: Cmd

parent_tokens

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

TYPE: Mapping[str, MutableSequence[str]] | None DEFAULT: None

Source code in cmd2/argparse_completer.py
def __init__(
    self,
    parser: Cmd2ArgumentParser,
    cmd_app: "Cmd",
    *,
    parent_tokens: Mapping[str, MutableSequence[str]] | None = None,
) -> None:
    """Create an ArgparseCompleter.

    :param parser: Cmd2ArgumentParser instance
    :param cmd_app: reference to the cmd2.Cmd instance that owns this ArgparseCompleter
    :param parent_tokens: optional Mapping of 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._cmd_app = cmd_app

    if parent_tokens is None:
        parent_tokens = {}
    self._parent_tokens = parent_tokens

    # All flags in this command
    self._flags: list[str] = []

    # Maps flags to the argparse action object
    self._flag_to_action: dict[str, argparse.Action] = {}

    # Actions for positional arguments (by position index)
    self._positional_actions: list[argparse.Action] = []

    # This will be set if self._parser has subcommands
    self._subcommand_action: argparse._SubParsersAction[Cmd2ArgumentParser] | None = None

    # 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

Sequence of argument tokens being passed to the parser

TYPE: Sequence[str]

cmd_set

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

TYPE: CommandSet[Any] | None DEFAULT: None

RETURNS DESCRIPTION
Completions

a Completions object

RAISES DESCRIPTION
CompletionError

for various types of completion errors

Source code in cmd2/argparse_completer.py
def complete(
    self,
    text: str,
    line: str,
    begidx: int,
    endidx: int,
    tokens: Sequence[str],
    *,
    cmd_set: CommandSet[Any] | None = None,
) -> Completions:
    """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: Sequence of argument tokens being passed to the parser
    :param cmd_set: if completing a command, the CommandSet the command's function belongs to, if applicable.
                    Defaults to None.
    :return: a Completions object
    :raises CompletionError: for various types of completion errors
    """
    if not tokens:
        return Completions()

    # 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: _ArgumentState | None = None

    # _ArgumentState of the current flag
    flag_arg_state: _ArgumentState | None = None

    # Non-reusable flags that we've parsed
    used_flags: set[str] = set()

    # Keeps track of arguments we've seen and any tokens they consumed
    consumed_arg_values: dict[str, list[str]] = {}

    # Completed mutually exclusive groups
    completed_mutex_groups: dict[argparse._MutuallyExclusiveGroup, argparse.Action] = {}

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

    #############################################################################################
    # 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, token)
            continue

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

        # Handle '--' which tells argparse all remaining arguments are non-flags
        if token == "--" and not skip_remaining_flags:  # noqa: S105
            # 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
            flag_arg_state = None
            skip_remaining_flags = True
            continue

        # Check if token is a flag
        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:
                self._update_mutex_groups(action, completed_mutex_groups, used_flags, remaining_positionals)

                # Check if the action type allows the same flag to be provided multiple times.
                # Reusable actions (append, count, extend) preserve their history so the
                # completion logic knows which values have already been 'consumed'.
                if not isinstance(
                    action,
                    (
                        argparse._AppendAction,
                        argparse._AppendConstAction,
                        argparse._CountAction,
                        argparse._ExtendAction,
                    ),
                ):
                    # For standard 'overwrite' actions (e.g., --store), providing the flag
                    # again resets its state. We mark the flags as 'used' to potentially
                    # filter them from future completion results and clear any previously
                    # recorded values for this destination.
                    used_flags.update(action.option_strings)
                    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:
                    flag_arg_state = new_arg_state
                    skip_remaining_flags = flag_arg_state.is_remainder

        # Check if token is a flag's argument
        elif flag_arg_state is not None:
            consume_argument(flag_arg_state, token)

            # Check if we have finished with this flag
            if flag_arg_state.count >= flag_arg_state.max:
                flag_arg_state = None

        # Otherwise treat token 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 and remaining_positionals:
                action = remaining_positionals.popleft()

                # Are we at a subcommand? If so, forward to the matching completer
                if self._subcommand_action is not None and action == self._subcommand_action:
                    if token in self._subcommand_action.choices:
                        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 = self._subcommand_action.choices[token]
                        completer = parser.completer_class(parser, self._cmd_app, parent_tokens=parent_tokens)
                        return completer.complete(text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set)

                    # Invalid subcommand entered, so no way to complete remaining tokens
                    return Completions()

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

            # Check if we have a positional to consume this token
            if pos_arg_state is not None:
                self._update_mutex_groups(pos_arg_state.action, completed_mutex_groups, used_flags, remaining_positionals)
                consume_argument(pos_arg_state, token)

                # 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 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
    #############################################################################################
    return self._handle_last_token(
        text,
        line,
        begidx,
        endidx,
        flag_arg_state,
        pos_arg_state,
        remaining_positionals,
        consumed_arg_values,
        used_flags,
        skip_remaining_flags,
        cmd_set,
    )

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: Sequence[str]

RETURNS DESCRIPTION
Completions

a Completions object

Source code in cmd2/argparse_completer.py
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: Sequence[str]) -> Completions:
    """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: a Completions object
    """
    # 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 = self._subcommand_action.choices[token]
                completer = parser.completer_class(parser, self._cmd_app)
                return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :])

            if token_index == len(tokens) - 1:
                # Since this is the last token, we will attempt to complete it
                return self._cmd_app.basic_complete(text, line, begidx, endidx, self._subcommand_action.choices)
            break
    return Completions()

print_help

print_help(tokens, file=None)

Supports cmd2's help command in the printing of help text.

PARAMETER DESCRIPTION
tokens

arguments passed to help command

TYPE: Sequence[str]

file

optional file object where the argparse should write help text If not supplied, argparse will write to sys.stdout.

TYPE: IO[str] | None DEFAULT: None

Source code in cmd2/argparse_completer.py
def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None:
    """Supports cmd2's help command in the printing of help text.

    :param tokens: arguments passed to help command
    :param file: optional file object where the argparse should write help text
                 If not supplied, argparse will write to sys.stdout.
    """
    # 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 tokens and self._subcommand_action is not None:
        parser = self._subcommand_action.choices.get(tokens[0])
        if parser is not None:
            completer = parser.completer_class(parser, self._cmd_app)
            completer.print_help(tokens[1:], file)
            return
    self._parser.print_help(file)

set_default_argparse_completer

set_default_argparse_completer(completer_class)

Set the default ArgparseCompleter class for a cmd2 app.

PARAMETER DESCRIPTION
completer_class

Type that is a subclass of ArgparseCompleter.

TYPE: type[ArgparseCompleter]

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

    :param completer_class: Type that is a subclass of ArgparseCompleter.
    """
    global DEFAULT_ARGPARSE_COMPLETER  # noqa: PLW0603
    DEFAULT_ARGPARSE_COMPLETER = completer_class