mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
fix(e2b): grep literal patterns verbatim instead of re-escaped (#6952)
E2BSandbox.grep() applied re.escape() to the caller's pattern and then handed the escaped text to grep -F. Fixed-string grep treats the added backslashes as part of the text to find, so an exact literal such as 'alpha.beta' never matched. Align with the boxlite/tenki/opensandbox seams: grep receives the RAW pattern (-F matches it literally, -E as a regex), and only a non-literal pattern is compile-validated locally. A shared-contract regression covers dot/space/bracket/backslash/plus literals through every shell-backed provider.
This commit is contained in:
parent
4f1c848744
commit
c3ac4b981f
@ -462,8 +462,14 @@ class E2BSandbox(Sandbox):
|
||||
case_sensitive: bool = False,
|
||||
max_results: int = 100,
|
||||
) -> tuple[list[GrepMatch], bool]:
|
||||
regex_source = re.escape(pattern) if literal else pattern
|
||||
re.compile(regex_source, 0 if case_sensitive else re.IGNORECASE)
|
||||
# Sanity-check a regex pattern as a Python regex at the boundary (grep uses
|
||||
# POSIX ERE, but this catches gross errors); a literal needs no validation.
|
||||
# grep receives the RAW pattern: -F matches it literally, -E as a regex.
|
||||
# Escaping it first would double-escape -- fixed-string grep would then
|
||||
# search for the added backslashes as text (``alpha\.beta``) and never
|
||||
# match the exact line (#5626).
|
||||
if not literal:
|
||||
re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
||||
|
||||
resolved = self._resolve_path(path)
|
||||
# Build a portable ``grep`` invocation:
|
||||
@ -492,7 +498,7 @@ class E2BSandbox(Sandbox):
|
||||
total_cap = max(max_results * 4, max_results + 50)
|
||||
flags.append(f"-m{per_file_cap}")
|
||||
|
||||
search = "grep " + " ".join(flags) + f" -- {shlex.quote(regex_source)} {shlex.quote(resolved)} 2>/dev/null"
|
||||
search = "grep " + " ".join(flags) + f" -- {shlex.quote(pattern)} {shlex.quote(resolved)} 2>/dev/null"
|
||||
|
||||
with self._lock:
|
||||
client = self._client
|
||||
|
||||
@ -235,6 +235,32 @@ def test_grep_case_insensitive_and_file_scope(provider, tree):
|
||||
assert truncated is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pattern", "line"),
|
||||
[
|
||||
("alpha.beta", "alpha.beta"),
|
||||
("hello world", "hello world"),
|
||||
("x[0]", "x[0]"),
|
||||
("path\\file", "path\\file"),
|
||||
("a+b", "a+b"),
|
||||
],
|
||||
ids=["dot", "space", "brackets", "backslash", "plus"],
|
||||
)
|
||||
def test_grep_literal_matches_regex_metacharacters(provider, tmp_path, pattern, line):
|
||||
# A literal pattern is matched verbatim: the sandbox must hand the RAW
|
||||
# pattern to ``grep -F``. Re-escaping it first (``alpha\.beta``) makes
|
||||
# fixed-string grep search for the backslash as part of the text, so an
|
||||
# exact line never matches (#5626).
|
||||
root = tmp_path / "literal"
|
||||
root.mkdir()
|
||||
(root / "sample.txt").write_text(f"header\n{line}\ntrailer\n", encoding="utf-8")
|
||||
|
||||
matches, truncated = provider.sandbox.grep(str(root), pattern, literal=True)
|
||||
|
||||
assert [(m.line_number, m.line) for m in matches] == [(2, line)]
|
||||
assert truncated is False
|
||||
|
||||
|
||||
def test_grep_glob_filter_is_root_relative(provider, tree):
|
||||
matches, truncated = provider.sandbox.grep(str(tree), "needle", glob="nested/*.txt")
|
||||
assert [(m.path, m.line_number) for m in matches] == [(str(tree / "nested" / "result.txt"), 1)]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user