Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
17 changes: 16 additions & 1 deletion cmd2/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
class CompletionItem:
"""A single completion result."""

_SANITIZE_RE = re.compile(r'\r\n|[\n\r\t\f\v]')

# The underlying object this completion represents (e.g., str, int, Path).
# This is used to support argparse choices validation.
value: Any = field(kw_only=False)
Expand All @@ -76,6 +78,15 @@ class CompletionItem:
display_plain: str = field(init=False)
display_meta_plain: str = field(init=False)

@classmethod
def _sanitize_display_string(cls, val: str) -> str:
"""Sanitize a string for display in the completion menu.

This replaces whitespace characters that are rendered as
control sequences (like ^J or ^I) with spaces.
"""
return cls._SANITIZE_RE.sub(' ', val)

def __post_init__(self) -> None:
"""Finalize the object after initialization."""
# Derive text from value if it wasn't explicitly provided
Expand All @@ -86,7 +97,11 @@ def __post_init__(self) -> None:
if not self.display:
object.__setattr__(self, "display", self.text)

# Pre-calculate plain text versions by stripping ANSI sequences.
# Sanitize display and display_meta
object.__setattr__(self, "display", self._sanitize_display_string(self.display))
object.__setattr__(self, "display_meta", self._sanitize_display_string(self.display_meta))

# Create plain text versions by stripping ANSI sequences.
# These are stored as attributes for fast access during sorting/filtering.
object.__setattr__(self, "display_plain", su.strip_style(self.display))
object.__setattr__(self, "display_meta_plain", su.strip_style(self.display_meta))
Expand Down
19 changes: 19 additions & 0 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,25 @@ def test_plain_fields() -> None:
assert completion_item.display_meta_plain == "A tasty apple"


def test_sanitization() -> None:
"""Test display string sanitization in CompletionItem."""
# Test all problematic characters being replaced by a single space.
# Also verify that \r\n is replaced by a single space.
display = "str1\r\nstr2\nstr3\rstr4\tstr5\fstr6\vstr7"
expected = "str1 str2 str3 str4 str5 str6 str7"

# Since display defaults to text if not provided, we test both text and display fields
completion_item = CompletionItem("item", display=display, display_meta=display)
assert completion_item.display == expected
assert completion_item.display_meta == expected

# Verify that text derived display is also sanitized
text = "item\nwith\nnewlines"
expected_text_display = "item with newlines"
completion_item = CompletionItem(text)
assert completion_item.display == expected_text_display


def test_styled_completion_sort() -> None:
"""Test that sorting is done with the display_plain field."""

Expand Down
Loading