Repository navigation
feat: head: support -c/--bytes - #135
Conversation
head -c N / --bytes=N writes the first N bytes of the input, byte-exact, without line splitting. When both -n and -c are given, the last one wins, as in coreutils. The negative form (-c -N) is not supported. Closes strands-agents#133
|
@strandly-the-agent review pls |
There was a problem hiding this comment.
Approve — the byte path is correct and byte-exact, and every GNU divergence fails loudly instead of silently.
I ran the suite plus a 20-case adversarial probe against f4f124a: 1303/1303 integration tests pass, -n/-c last-wins holds in both orders, head -c 2 on héllo emits 2 bytes (splits the multibyte char, as coreutils does), and -c -5 / -c 1K / -c abc each exit 1 with a parse error — no silent wrong answers anywhere. The #133 web_fetch pattern at 5 MiB yields exactly 5242880 bytes.
Two non-blocking things: the -n … -c order isn't pinned by a test (inline), and the published command reference still says head has "No -c".
✅ What I verified (f4f124a)
cargo test --test shell_integration → 1303 passed, 0 failed (9 head_ tests). No local rustfmt/clippy components in my sandbox, but CI is green on all 20 checks.
Adversarial probe, 20 cases, run locally against f4f124a (script + output uploaded to the Strandly artifacts store as probe-head-c.sh / probe-head-c.out):
| Case | Result |
|---|---|
head -c 4 on ab\n\ncd\n |
4 bytes, newlines intact |
head -c 2 on héllo (6 bytes) |
2 bytes — splits the é, matches coreutils |
-c2, --bytes=2, -c 0, N > input, -c 18446744073709551615 |
all correct |
head -c 1 -n 2 / head -n 2 -c 1 / -c 5 -n 2 -c 3 |
a\nb / a / a\nb — last wins both ways |
-c -5, -c 1K, -c abc, -c 10^26 |
exit 1 + cannot parse argument …; never a truncated-wrong answer |
| missing file / directory | no such file or directory / is a directory, exit 1 |
| 100 KB through a pipe (12× the 8 KiB copy buffer) | 100000 bytes |
head -c 5242880 OUT > OUT.part && mv OUT.part OUT on a 6 MiB file |
5242880 bytes — the #133 web_fetch case |
over max_file_size (10 MiB) |
head: file size limit exceeded on stderr, not silence |
head -c 5 /dev/zero |
terminates — take(c) never over-reads, unlike the -n path on a newline-free stream |
On move semantics/flush, since they're easy to get wrong here: take(c) consuming the Box is fine because that branch diverges, so reader is still live for BufReader::new at head.rs:43. And tokio::io::copy does poll_flush at EOF — which matters because a > redirect makes stdout a tokio::fs::File, and that needs a flush before drop. head_bytes_redirect is the only test covering that writer, so it's worth keeping even though it looks like it's testing > and mv.
Questions (none blocking)
- Docs say the opposite.
strands-agents/harness-sdk→site/src/content/docs/user-guide/shell/commands.mdxlists theheadgap as "No-c, negative-n, orhead -5shorthand", which goes stale the moment this merges. The PR says Documentation PR: N/A — worth a one-line follow-up there (I didn't open it since it's a different repo). - GNU size suffixes.
head -c 1K/-c 5Mare rejected withcannot parse argument "1K": invalid digit found in string. #133 only needs plain bytes, and a clear error is the right failure mode — but an agent prompted on GNUheadwill reach for-c 1M. Accept suffixes in a follow-up, or is plain-only deliberate? tail -c. Stilltail: invalid option '-c'. Agents tend to use the pair symmetrically; worth tracking as a sibling issue?
Appendix — non-blocking (3)
- ⚪
head -c 3 -opens./-instead of stdin (no such file or directory: /home/lash/-). Pre-existing and shell-wide (cat -,tail -do the same), not this PR. - ⚪
head -c N f1 f2is still a hard error with no==>headers — pre-existing, already a documented gap. - ⚪ Pre-existing, not filed: the command immediately after a large (≳2 MiB) redirect reads the new file's size as
0; the next one sees it correctly. Head-free repro that works onmain:No data is lost (the 5 MiB$ strands-shell -c "jq -nr '\"x\" * 2097152' > /tmp/A; sleep 1; cat /tmp/A > /tmp/B; wc -c /tmp/B; sleep 1; wc -c /tmp/B" 0 /tmp/B 2097153 /tmp/Bweb_fetchpattern above ends up byte-correct), butcmd > out; wc -c outreporting 0 is a silent wrong answer an agent would act on. Happy to file it separately if you want it tracked.
Description
headaccepted only-n/--lines. This adds-c N/--bytes=N, which writes the first N bytes of the file or stdin, byte-exact (streamed viatokio::io::copyovertake(N), no line splitting or UTF-8 decoding). When both-nand-care given, the last one wins, as in coreutils. The negative form (-c -N) is not supported, per the issue.The Strands harness
web_fetchtool caps downloaded bodies withhead -c 5242880 OUT > OUT.part && mv -f OUT.part OUT. Themv -fhalf is tracked separately in #134.Related Issues
Closes #133
Documentation PR
N/A
Type of Change
New feature
Testing
Added integration tests in
tests/shell_integration.rs: file input (exact-bytes assertion including newlines), stdin,--bytes=, N larger than input,-c 0, last-option-wins, and the> OUT.part && mvredirect pattern.cargo test --workspace --all-targets,pytest tests/python,npm test)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.