Conversation
|
Pushed efce0d9, which fixes a few places where the first commit differed from GNU grep 3.11 (checked against
The description is updated, including the remaining known differences (e.g. |
grep read lines into a String, so the first non-UTF-8 file failed read_line with InvalidData, and the `?` in cmd_grep aborted the file loop: the command exited 1 and every later file was never scanned. Read lines as bytes and treat binary input like GNU grep: a NUL in the first buffer marks the file binary, and invalid-UTF-8 lines are matched lossily but never printed. Either way a match reports "grep: FILE: binary file matches" on stderr. Add -a/--text, -I and --binary-files=TYPE. Per-file read errors are reported and grep moves on; BrokenPipe still stops it. Fixes strands-agents#123 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXaT7zwomYh4CtqSGrCY1Y
Traced against the GNU grep 3.11 source (src/grep.c): - Match on raw bytes (regex::bytes), so `.` never matches an invalid byte: `a\xffb` no longer matches `a.b`. - With -o, hide only matches whose own bytes are invalid UTF-8; valid matches on such a line still print (print_line_middle). - Detect NULs in the first 96 KiB, as GNU does with INITIAL_BUFSIZE, and treat a later NUL as making the rest of the file binary. With -I the file then counts as non-matching. - -q/-l/-L stop at the first match. -a still decodes invalid bytes lossily, because captured shell output must be valid UTF-8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXaT7zwomYh4CtqSGrCY1Y
efce0d9 to
5dc6163
Compare
strandly-the-agent
left a comment
There was a problem hiding this comment.
Verdict: comment — no blockers. The #123 abort is genuinely fixed and the plain-text path is untouched: 48 text-only invocations are byte-identical to main, and NUL-offset behaviour matches GNU 3.11 at 9 KB / 50 KB / 118 KB. Two 🟡 consistency bugs in the new binary path, both inline:
grep -Ion a non-UTF-8 file can exit 0 with empty stdout and empty stderr (src/commands/grep.rs:306).- A hidden binary line evicts valid text before-context lines the user asked for (
src/commands/grep.rs:330) — the "still occupy a context slot" comment undersells this, and your known-differences list covers separators, not dropped lines.
Non-blocking design question: hiding matches on invalid-UTF-8 lines in otherwise-text files (a Latin-1 source file) means stdout shows nothing and only stderr carries the note. GNU in C.UTF-8 prints the line. For an agent that reads stdout, is stderr-only the right channel for "I found it but won't show you"? -a is the escape hatch, but the agent has to know to reach for it.
✅ What I verified (head 5dc6163, base 8f29276)
cargo test --test shell_integration→ 1317 passed, 0 failed;grep_filter → 94 passed.- #123 repro on a directory containing
\xffand\0files:main→ exit 1, 0 matching lines,grep: stream did not contain valid UTF-8; this branch → exit 0, every file scanned. - 48 plain-text invocations (
-n -c -v -o -A -B -C -m -l -L -H -h -e -F -i -w -x -r, plus combinations) diffedmainvs branch: identical stdout+stderr+exit in all but the CRLF case in appendix 2. - NUL offsets vs GNU grep 3.11: NUL at byte 9,510 / 49,910 / 117,910 → stdout, stderr and exit identical to GNU in all three, so
fill_buf()really does deliver the full 96 KiB through the VFS. Late NUL arriving over a pipe on stdin also matches GNU. - Your stderr claim checks out:
take_writerremoves the fd (src/os.rs:471-479), and onmaina second unreadable file loses its message entirely —grep needle nope1 nope2prints one line there, both here. That's a second real bug fixed by this PR. - BrokenPipe:
grep needle big.txt | head -n 2→ 2 lines, exit 0, no per-file repetition. - Full log uploaded as
pr124-verification.txt(commands + raw output for every claim above).
Appendix — non-blocking (6)
- ⚪
-o+-Aloses the note.binary_matched |= !opts.only_matching;at:403and its comment assume-oprints no context — but it does here:printf 'foo\nbar\n' | grep -o -A1 fooprintsfooandbar(GNU prints onlyfoo). Sogrep -o -A1 needle filehides an invalid after-context line with no note at all, while-o -B1reports it via:351.binary_matched = true;fixes the inconsistency. - ⚪ CRLF.
strip_suffixat:294-295replacesmain'strim_end_matches, so a line ending\r\r\nnow keeps one\r(mainstripped both; GNU keeps both). Closer to GNU — just not in your differences list. - ⚪ Pre-existing, not introduced here: spurious
--on pure text.printf 'needle A\nl2\nl3\nl4\nneedle B\n' > f; grep -B3 needle fprintsneedle A/--/l2/l3/l4/needle B; GNU emits no separator because the groups are adjacent. Identical onmain((opts.before_context == 0 || !before_buf.is_empty()) && need_sepat:345). Not filed — worth a one-line issue if you agree. - ⚪ Pre-existing, and you already flag it in a test comment: single-file
-l/-Lprints an empty line instead of the filename (:444-449). - ⚪ One more GNU divergence not in your list:
printf '\0\xffneedle\n' | grep -vc needle→0, exit 1 here; GNU 3.11 says1, exit 0. Yours looks like the defensible answer. - Scope. +155 lines and three new flags is more than the minimal "stop aborting" repair, but suppression is what keeps binary bytes out of an agent's context, and
-a/-Iare the escape hatches an agent will actually try. I'd keep it.
Reading order for a human reviewer
src/commands/grep.rs:255-283 (detection + flags) → :284-338 (hidden-match path, where both 🟡s live) → :397-435 (context slot accounting) → tests/shell_integration.rs:2143-2213 (context tests, which encode the current semantics).
Caveat on this review: the independent reviewer / adversarial subagents weren't available in this run, so all passes were run inline by me. The context state machine in particular deserves a human eye.
| } | ||
| } | ||
| // Invalid UTF-8 is matched on raw bytes but never printed unless -a. | ||
| let invalid = detect_binary && std::str::from_utf8(raw).is_err(); |
There was a problem hiding this comment.
grep -I on a non-UTF-8 file can report success with no output at all.
A line that is invalid UTF-8 but has no NUL still counts as a match (found = true, match_count += 1), is then hidden, and under --binary-files=without-match the stderr note at :450 is suppressed too:
$ printf 'caf\xe9 needle\nplain\n' > latin1.txt
$ grep -I needle latin1.txt # exit 0, stdout empty, stderr empty
$ grep -cI needle latin1.txt # 1
$ grep -lI needle latin1.txt # lists the file
So -I reports matching for exactly the input it is meant to treat as non-matching, and the caller gets no signal at all. The NUL path at :298-302 already handles this by zeroing match_count/found; doing the same when a line is hidden as invalid would make -I consistent (exit 1, -c → 0). tests/shell_integration.rs:2209 only covers a file that also has valid matching lines, so this case isn't pinned.
There was a problem hiding this comment.
GNU 3.11 (LC_ALL=C.UTF-8) behaves the same: -I only acts on NUL detection (src/grep.c:1558), and lines with encoding errors are dropped at print time with no note. On your latin1.txt, grep -I needle gives empty stdout, empty stderr, exit 0, and grep -cI prints 1. Leaving it for GNU parity.
| if use_context { | ||
| // The hidden match ends any open context and breaks the group. | ||
| after_remaining = 0; | ||
| before_buf.clear(); |
There was a problem hiding this comment.
A hidden binary line discards valid text before-context lines inside the requested window.
before_buf.clear() here (and at :408 for the non-matching case) throws away lines the user explicitly asked for:
$ printf 'l1\nl2\n\xffneedle hid\nneedle vis\n' > hidm.txt
$ grep -B2 needle hidm.txt
needle vis # l1 and l2 are gone; GNU 3.11 prints both
$ printf 'needle A\nl2\n\xffbin\nl4\nneedle B\n' > sep4.txt
$ grep -B3 needle sep4.txt # needle A / -- / l4 / needle B; GNU keeps l2
Those dropped lines are plain text, and the only signal is binary file matches, which reads as "nothing printable here", not "I also withheld 2 text lines". :398's "they still occupy a context slot" describes slot accounting, not eviction.
Suggestion: keep before_buf intact and let the ring-buffer window drop lines naturally — last_binary_lineno already tracks whether the note is owed.
There was a problem hiding this comment.
Good catch, fixed in 60f6373. A hidden line now keeps its slot in the window instead of clearing the buffer, following GNU's lastout model. Both of your examples now match GNU 3.11 output, and the new tests pin them. The same commit fixes appendix 1 (-o writes no context, so the note is reported) and appendix 3 (spurious -- between adjacent groups).
Hidden (binary) lines were clearing the before-context buffer, so valid text lines the user asked for with -B/-C were dropped. They now keep their slot in the window without being written. Context output follows GNU grep's prtext/lastout model: - `--` is written only when the next group is not contiguous with the last line actually written. - A hidden match starts no after-context, and a hidden line inside the after-context ends it. - -o writes no context lines (GNU), which also restores the binary note for -o -A. Two context bugs from main are fixed along the way: a spurious `--` between adjacent groups, and -o printing context lines. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AXaT7zwomYh4CtqSGrCY1Y
|
Thanks for the thorough review. On the stdout question: GNU 3.11 in Appendices 2 (CRLF) and 5 are both in the description's known differences. Appendix 5 is the NUL-as-line-break one: GNU splits |
|
@strandly-the-agent we have an error running strands shell in a workspace with binary files grep errors, any update? |
Description
grepread every line into aString, so the first file that wasn't valid UTF-8 maderead_linefail withInvalidData.cmd_grepthen propagated that with?, which aborted the whole file loop: the command exited 1 withgrep: stream did not contain valid UTF-8, and every file after the offending one (in directory order) was silently never scanned.This change makes
grepbyte-oriented and handles binary input the way GNU grep 3.11 does (UTF-8 locale; checked against both its output andsrc/grep.c):read_until(b'\n'), so file content can no longer makegrepfail.INITIAL_BUFSIZE) marks the whole file binary. Its lines are never printed; a match producesgrep: FILE: binary file matcheson stderr, with exit status 0, and reading stops after the first match. A NUL found later makes the rest of the file binary from that line on; lines already printed stay printed, and under-Ithe file counts as non-matching.regex::bytes), so.never matches an invalid byte. A line with invalid UTF-8 is not printed; the file's other lines print normally, followed by the same stderr note. With-o, only matches whose own bytes are invalid are hidden.-c,-l,-Land-qcount binary matches as usual and print no note;-q/-l/-Lstop at the first match.-A/-B/-C) follows GNU's model: a hidden line keeps its slot in the window but is never written,--only appears when a group isn't contiguous with the last line written, and-owrites no context lines. This also fixes two context bugs already onmain: a spurious--between adjacent groups, and-oprinting context.-a/--text,-I, and--binary-files=binary|text|without-match(a smallBinaryFilesenum).grep: PATH: ERRandgrepmoves on to the next file, the same way open errors are already handled. A closed stdout (BrokenPipe, e.g.grep -r … | head -n 1) still stopsgrepstraight away.greptakes stderr once up front and shares it across files. Previously it calledio::stderr()for each open error, and the second call failed because the writer had already been taken.Before (0.3.3) and after, using the repro from #123 (Node binding,
directread-only bind):Known differences from GNU grep (not addressed here)
-aprints invalid bytes as U+FFFD rather than raw bytes, because captured shell output must be valid UTF-8 (a raw byte would empty the wholestdout, as already happens withcaton such a file).printf 'a\0a\n' | grep -c agives 2 there and 1 here.-A) prints the lines before it as after-context, an artifact of its unsetlastout. This isn't copied.\ris stripped before matching (mainstripped every trailing\r, this strips one), whereas GNU keeps it. CRLF files therefore print without\r.Out of scope, possible follow-ups
-rvisits files in unsorted directory order, as before.cargo clippy --workspace --all-targets -- -D warningsalready fails onmain(find.rs, lua.rs, ls.rs, exec.rs). This PR adds no new warnings.Related Issues
Fixes #123
Documentation PR
None yet. The command reference page could list the new
-a,-Iand--binary-filesoptions.Type of Change
Bug fix
Testing
New integration tests in
tests/shell_integration.rs. They bind a host directory of raw-byte fixtures (\0,\xff) and cover: the [BUG]grep -raborts the whole command (and silently drops matches from other files) when a bound directory contains a non-UTF-8 file #123 repro (recursive search continues past a binary file), invalid UTF-8 lines being hidden,-I/-a/--binary-files,-c/-l/-q, the stdin(standard input)note, context windows around hidden lines (pinned to GNU output), a binary file with no match (exit 1, no note),BrokenPipestopping after the first file,.not matching an invalid byte,-oon invalid lines, and a NUL after the first 96 KiB.Ran the [BUG]
grep -raborts the whole command (and silently drops matches from other files) when a bound directory contains a non-UTF-8 file #123 repro end to end against a localnpm run build:debugof the Node addon (output above).Compared against GNU grep 3.11 (
debian:stable-slim,LC_ALL=C.UTF-8) and BSD grep 2.6.0, including a 273-case matrix (21 fixtures × 13 option sets) that is byte-identical to GNU except for the known differences above, and traced the binary and encoding-error paths in the grep 3.11 source (buf_has_nulls,print_line_head,print_line_middle).I ran the relevant test suites for the bindings I touched (
cargo test --workspace --all-targets,pytest tests/python,npm test): Rust core only, no binding surface changed. Rancargo test(all pass) andnpm test(41/41); pytest not run locally.If I touched Rust, I ran
cargo fmtandcargo clippyChecklist
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AXaT7zwomYh4CtqSGrCY1Y