Hi cmd2 team — found a module-level state mutation bug in cmd2/cmd2.py while running adversarial test generation via tailtest. One root cause, four copy-pasted source locations, compounding effect across calls.
Bug
Four functions assign constants.REDIRECTION_TOKENS by reference then .extend() it with terminators:
| Function |
Line |
_alias_create |
3821 |
_alias_list |
3901 |
_macro_create |
4068 |
_macro_list |
4191 |
Each is shaped like:
tokens_to_unquote = constants.REDIRECTION_TOKENS # reference, not copy
tokens_to_unquote.extend(self.statement_parser.terminators)
Every call to these four functions permanently appends terminators to the module-level constants.REDIRECTION_TOKENS. Over multiple calls the list grows without bound:
After 0 calls: ['|', '>', '>>']
After 1 call: ['|', '>', '>>', ';']
After 5 calls: ['|', '>', '>>', ';', ';', ';', ';', ';', ';', ';', ';']
Impact
- Compounding mutation: Each alias/macro create/list call pollutes the global list.
- Cross-instance corruption: Two
Cmd() instances in the same process share the corrupted state. Instance A's alias operations affect instance B's redirection parsing.
- Unbounded growth in long-running sessions: Each invocation adds duplicates.
Fix
In all 4 locations, change:
tokens_to_unquote = constants.REDIRECTION_TOKENS
to:
tokens_to_unquote = list(constants.REDIRECTION_TOKENS)
Reproduction
import pytest
from cmd2 import Cmd
from cmd2 import constants
def test_alias_create_does_not_mutate_redirection_tokens():
"""alias create should not mutate the module-level REDIRECTION_TOKENS list."""
snapshot = list(constants.REDIRECTION_TOKENS)
app = Cmd()
app.onecmd_plus_hooks("alias create myalias echo hello")
assert constants.REDIRECTION_TOKENS == snapshot, (
f"BUG: REDIRECTION_TOKENS mutated by alias create. "
f"Was {snapshot!r}, now {constants.REDIRECTION_TOKENS!r}"
)
def test_repeated_alias_creates_compound_mutation():
"""Compound effect: 5 alias creates should not grow REDIRECTION_TOKENS."""
snapshot = list(constants.REDIRECTION_TOKENS)
app = Cmd()
for i in range(5):
app.onecmd_plus_hooks(f"alias create alias_{i} echo {i}")
assert constants.REDIRECTION_TOKENS == snapshot, (
f"BUG: REDIRECTION_TOKENS grew over 5 alias creates: "
f"{snapshot!r} -> {constants.REDIRECTION_TOKENS!r}"
)
def test_two_instances_alias_create_independence():
"""Two Cmd instances should not share corrupted REDIRECTION_TOKENS state."""
snapshot = list(constants.REDIRECTION_TOKENS)
a = Cmd()
b = Cmd()
a.onecmd_plus_hooks("alias create x echo x")
state_after_a = list(constants.REDIRECTION_TOKENS)
assert state_after_a == snapshot, (
f"BUG: instance A mutated module-level REDIRECTION_TOKENS: "
f"{snapshot!r} -> {state_after_a!r}; instance B sees the corruption"
)
Happy to open a PR — it's a 4-line change. Found via tailtest adversarial test generation.
Hi cmd2 team — found a module-level state mutation bug in
cmd2/cmd2.pywhile running adversarial test generation via tailtest. One root cause, four copy-pasted source locations, compounding effect across calls.Bug
Four functions assign
constants.REDIRECTION_TOKENSby reference then.extend()it with terminators:_alias_create_alias_list_macro_create_macro_listEach is shaped like:
Every call to these four functions permanently appends terminators to the module-level
constants.REDIRECTION_TOKENS. Over multiple calls the list grows without bound:Impact
Cmd()instances in the same process share the corrupted state. Instance A's alias operations affect instance B's redirection parsing.Fix
In all 4 locations, change:
to:
Reproduction
Happy to open a PR — it's a 4-line change. Found via tailtest adversarial test generation.