From 267381bc9bcd2b0606753241d6001c92cd4cec60 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 Apr 2026 10:31:56 -0400 Subject: [PATCH 1/2] Fixed ArgparseCompleter.print_help() not passing file stream to recursive call. --- CHANGELOG.md | 5 +++++ cmd2/argparse_completer.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f01257c0..4cafd4337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.5.1 (April 24, 2026) + +- Bug Fixes + - Fixed `ArgparseCompleter.print_help()` not passing file stream to recursive call. + ## 3.5.0 (April 13, 2026) - Bug Fixes diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 4d88b5f75..cbbd7fedf 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -637,7 +637,7 @@ def print_help(self, tokens: list[str], file: IO[str] | None = None) -> None: if parser: completer_type = self._cmd2_app._determine_ap_completer_type(parser) completer = completer_type(parser, self._cmd2_app) - completer.print_help(tokens[1:]) + completer.print_help(tokens[1:], file=file) return self._parser.print_help(file=file) From 601a2af99f0bb3efe8f808acc196fec551a92eac Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Fri, 24 Apr 2026 10:33:54 -0400 Subject: [PATCH 2/2] Fixed issue where constants.REDIRECTION_TOKENS was being mutated. --- CHANGELOG.md | 1 + cmd2/cmd2.py | 16 +++++----------- cmd2/constants.py | 6 +++--- cmd2/parsing.py | 20 ++++++++++---------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cafd4337..69b91d9a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Bug Fixes - Fixed `ArgparseCompleter.print_help()` not passing file stream to recursive call. + - Fixed issue where `constants.REDIRECTION_TOKENS` was being mutated. ## 3.5.0 (April 13, 2026) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 520c47b4a..2b37527c4 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1666,10 +1666,8 @@ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> tuple[li **On Failure** - Two empty lists """ - import copy - unclosed_quote = '' - quotes_to_try = copy.copy(constants.QUOTES) + quotes_to_try = [*constants.QUOTES] tmp_line = line[:endidx] tmp_endidx = endidx @@ -3721,8 +3719,7 @@ def _alias_create(self, args: argparse.Namespace) -> None: return # Unquote redirection and terminator tokens - tokens_to_unquote = constants.REDIRECTION_TOKENS - tokens_to_unquote.extend(self.statement_parser.terminators) + tokens_to_unquote = [*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators] utils.unquote_specific_tokens(args.command_args, tokens_to_unquote) # Build the alias value string @@ -3801,8 +3798,7 @@ def _alias_list(self, args: argparse.Namespace) -> None: """List some or all aliases as 'alias create' commands.""" self.last_result = {} # dict[alias_name, alias_value] - tokens_to_quote = constants.REDIRECTION_TOKENS - tokens_to_quote.extend(self.statement_parser.terminators) + tokens_to_quote = [*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators] to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.aliases, key=self.default_sort_key) @@ -3964,8 +3960,7 @@ def _macro_create(self, args: argparse.Namespace) -> None: return # Unquote redirection and terminator tokens - tokens_to_unquote = constants.REDIRECTION_TOKENS - tokens_to_unquote.extend(self.statement_parser.terminators) + tokens_to_unquote = [*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators] utils.unquote_specific_tokens(args.command_args, tokens_to_unquote) # Build the macro value string @@ -4087,8 +4082,7 @@ def _macro_list(self, args: argparse.Namespace) -> None: """List macros.""" self.last_result = {} # dict[macro_name, macro_value] - tokens_to_quote = constants.REDIRECTION_TOKENS - tokens_to_quote.extend(self.statement_parser.terminators) + tokens_to_quote = [*constants.REDIRECTION_TOKENS, *self.statement_parser.terminators] to_list = utils.remove_duplicates(args.names) if args.names else sorted(self.macros, key=self.default_sort_key) diff --git a/cmd2/constants.py b/cmd2/constants.py index 1ecd19374..18d10f6bb 100644 --- a/cmd2/constants.py +++ b/cmd2/constants.py @@ -7,12 +7,12 @@ # Used for command parsing, output redirection, tab completion and word # breaks. Do not change. -QUOTES = ['"', "'"] +QUOTES = ('"', "'") REDIRECTION_PIPE = '|' REDIRECTION_OUTPUT = '>' REDIRECTION_APPEND = '>>' -REDIRECTION_CHARS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT] -REDIRECTION_TOKENS = [REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND] +REDIRECTION_CHARS = (REDIRECTION_PIPE, REDIRECTION_OUTPUT) +REDIRECTION_TOKENS = (REDIRECTION_PIPE, REDIRECTION_OUTPUT, REDIRECTION_APPEND) COMMENT_CHAR = '#' MULTILINE_TERMINATOR = ';' diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 8f902c089..9e4a6dc9d 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -298,10 +298,12 @@ def __init__( # the string (\Z matches the end of the string even if it # contains multiple lines) # - invalid_command_chars = [] - invalid_command_chars.extend(constants.QUOTES) - invalid_command_chars.extend(constants.REDIRECTION_CHARS) - invalid_command_chars.extend(self.terminators) + invalid_command_chars = ( + *constants.QUOTES, + *constants.REDIRECTION_CHARS, + *self.terminators, + ) + # escape each item so it will for sure get treated as a literal second_group_items = [re.escape(x) for x in invalid_command_chars] # add the whitespace and end of string, not escaped because they @@ -352,9 +354,8 @@ def is_valid_command(self, word: str, *, is_subcommand: bool = False) -> tuple[b return False, errmsg errmsg = 'cannot contain: whitespace, quotes, ' - errchars = [] - errchars.extend(constants.REDIRECTION_CHARS) - errchars.extend(self.terminators) + + errchars = (*constants.REDIRECTION_CHARS, *self.terminators) errmsg += ', '.join([shlex.quote(x) for x in errchars]) match = self._command_pattern.search(word) @@ -677,9 +678,8 @@ def split_on_punctuation(self, tokens: list[str]) -> list[str]: :param tokens: the tokens as parsed by shlex :return: a new list of tokens, further split using punctuation """ - punctuation: list[str] = [] - punctuation.extend(self.terminators) - punctuation.extend(constants.REDIRECTION_CHARS) + # Using a set for faster lookups + punctuation = {*self.terminators, *constants.REDIRECTION_CHARS} punctuated_tokens = []