Skip to content

feat: head: support -c/--bytes - #135

Merged
mkmeral merged 1 commit into
strands-agents:mainfrom
ferdingler:feat/head-bytes
Oct 6, 2026
Merged

mkmeral merged 1 commit into
strands-agents:mainfrom
ferdingler:feat/head-bytes

Conversation

@ferdingler

Copy link
Copy Markdown
Contributor

Description

head accepted only -n/--lines. This adds -c N / --bytes=N, which writes the first N bytes of the file or stdin, byte-exact (streamed via tokio::io::copy over take(N), no line splitting or UTF-8 decoding). When both -n and -c are given, the last one wins, as in coreutils. The negative form (-c -N) is not supported, per the issue.

The Strands harness web_fetch tool caps downloaded bodies with head -c 5242880 OUT > OUT.part && mv -f OUT.part OUT. The mv -f half 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 && mv redirect pattern.

  • I ran the relevant test suites for the bindings I touched (cargo test --workspace --all-targets, pytest tests/python, npm test)
  • If I touched Rust, I ran cargo fmt and cargo clippy

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

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
@ferdingler
ferdingler requested a review from a team as a code owner October 6, 2026 00:00
@ferdingler
ferdingler requested a review from mkmeral October 6, 2026 00:00
@mkmeral

mkmeral commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

@strandly-the-agent review pls

@strandly-the-agent strandly-the-agent left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
  1. Docs say the opposite. strands-agents/harness-sdk → site/src/content/docs/user-guide/shell/commands.mdx lists the head gap as "No -c, negative -n, or head -5 shorthand", 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).
  2. GNU size suffixes. head -c 1K / -c 5M are rejected with cannot 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 GNU head will reach for -c 1M. Accept suffixes in a follow-up, or is plain-only deliberate?
  3. tail -c. Still tail: 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 f2 is 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 on main:
    $ 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/B
    
    No data is lost (the 5 MiB web_fetch pattern above ends up byte-correct), but cmd > out; wc -c out reporting 0 is a silent wrong answer an agent would act on. Happy to file it separately if you want it tracked.

Comment thread tests/shell_integration.rs
@mkmeral
mkmeral enabled auto-merge (squash) October 6, 2026 15:19
@mkmeral
mkmeral merged commit 1d1a0aa into strands-agents:main Oct 6, 2026
30 checks passed
@ferdingler
ferdingler deleted the feat/head-bytes branch October 6, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

head: support -c/--bytes

3 participants