Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 3.5.1 (April 24, 2026)

- 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)

- Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 5 additions & 11 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions cmd2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ';'

Expand Down
20 changes: 10 additions & 10 deletions cmd2/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = []

Expand Down
Loading