Skip to content

Commit 7b049fb

Browse files
fix: escape paths and commands in rich output
When the watcher detected a file path or executed a command containing square brackets (e.g., `file_with_[bracket].txt`), the `rich` library attempted to parse them as markup tags, causing a `rich.errors.MarkupError` and crashing the background debounce thread. This commit imports `escape` from `rich.markup` and applies it to dynamically generated strings injected into the console.print statements, ensuring safe output and robust execution. A unit test was added to verify paths and commands with brackets no longer crash the process. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: shenald-dev <245350826+shenald-dev@users.noreply.github.com>
1 parent abfabd1 commit 7b049fb

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

.jules/bolt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,11 @@ Using `.join()` unconditionally to replace `time.sleep()` in test cases is a fla
108108

109109
Action:
110110
Instead of `time.sleep()`, tests should use dynamic polling mechanisms (`while handler.current_process is None` coupled with short `time.sleep(0.05)` cycles and a maximum timeout) to efficiently wait only until the desired intermediate condition is met. This ensures the tests run significantly faster while preventing flakiness.
111+
112+
## 2026-04-24 — Rich Markup Error Bug
113+
114+
Learning:
115+
When passing raw user strings containing square brackets (like file paths, directories, or bash commands) into `rich.console.print` format strings, `rich` attempts to parse them as style markup tags (e.g., `[red]`). If the string inside the brackets is not a valid tag, or if there's a typo/unclosed tag, the library throws a `MarkupError` exception which will crash the thread executing the print statement.
116+
117+
Action:
118+
Always use `rich.markup.escape(str(variable))` before injecting unvalidated user-provided strings into `rich` print statements to guarantee safe output.

src/echo/watcher.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from watchdog.observers import Observer
1313
from watchdog.events import FileSystemEventHandler
1414
from rich.console import Console
15+
from rich.markup import escape
1516

1617
console = Console()
1718

@@ -122,7 +123,7 @@ def _run_command(self, event_path):
122123
if self.is_shutting_down:
123124
return
124125

125-
console.print(f"\n[cyan]📡 Change detected in {event_path}. Executing: [yellow]{self.command}[/][/cyan]")
126+
console.print(f"\n[cyan]📡 Change detected in {escape(str(event_path))}. Executing: [yellow]{escape(str(self.command))}[/][/cyan]")
126127
try:
127128
with self.process_lock:
128129
if self.is_shutting_down:
@@ -247,7 +248,7 @@ def main():
247248
observer = Observer()
248249
observer.schedule(event_handler, args.path, recursive=True)
249250

250-
console.print(f"[bold green]✨ Echo is watching [cyan]{args.path}[/] and will run [yellow]{args.cmd}[/][/bold green]")
251+
console.print(f"[bold green]✨ Echo is watching [cyan]{escape(str(args.path))}[/] and will run [yellow]{escape(str(args.cmd))}[/][/bold green]")
251252

252253
try:
253254
observer.start()

tests/test_markup.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import time
2+
from echo.watcher import CommandRunnerHandler
3+
from unittest.mock import MagicMock
4+
5+
def test_rich_markup_crash_escaping():
6+
# If the command has markup tags, it should not crash
7+
handler = CommandRunnerHandler("echo [/cyan]")
8+
9+
# Create a mock event with a bracket in the name
10+
mock_event = MagicMock()
11+
mock_event.is_directory = False
12+
mock_event.src_path = "file_with_[bracket].txt"
13+
mock_event.event_type = 'modified'
14+
15+
handler.on_any_event(mock_event)
16+
17+
# Wait for execution
18+
start_time = time.monotonic()
19+
while handler.current_process is None and time.monotonic() - start_time < 3.0:
20+
time.sleep(0.05)
21+
22+
assert handler.current_process is not None, "Process should have started, indicating it didn't crash"
23+
24+
if handler.current_process:
25+
handler.current_process.terminate()
26+
handler.current_process.wait()

0 commit comments

Comments
 (0)