Repository navigation
feat(interpreter): stream cat through pipelines - #2584
Merged
Merged
Conversation
Plain cat writes into the stage pipe as it reads and, as a middle stage, reads its input pipe incrementally through a new internal Context::stdin_stream. `cat big | head -1` and `loop | cat | head -1` now stop with SIGPIPE like bash. cat continues past a missing operand. Claude-Session: https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 01ff1d7 | Commit Preview URL Branch Preview URL |
Oct 07 2026, 06:08 PM |
4 tasks done
chaliy
added a commit
that referenced
this pull request
Oct 7, 2026
<!-- ccr-projects-attribution: {"github_login":"chaliy"} -->
_Requested by **Михайло** · [project
thread](https://claude.ai/code/project/chan_019YEkBJuZMHJdXrjom86cGX?thread=cmsg_019YEkBJuZMHJdXrjom86cGX6hNmzgZPpfCgsgRMptbhdM)_
## What changed
`grep` and `tr` now stream when they are a pipeline stage, following
plain `cat` (#2584). They read input in ~4 KiB pieces and write results
as they go. `grep` cuts its pieces on record ends and `tr` on UTF-8
character ends. `grep -m` stops reading once its budget is spent.
Options that need the whole input stay buffered and drain the pipe
first:
- `grep -c/-l/-L/-q`, context lines, `-b`, `-r`
- `tr -s`
The shared piece reader (`InputChunks`) lives in `builtins/mod.rs` so
more filters can adopt it.
## Why
This is roadmap step 2 (concurrent/streaming execution). `producer |
grep pat | head` is one of the most common agent pipelines. Before this
change, a `grep` or `tr` stage downstream of an endless or large
producer read everything, so it ran until a limit fired instead of
stopping at SIGPIPE.
## Before / After
| Script | Before | After (= bash) |
|---|---|---|
| `while :; do echo y; done \| grep y \| head -2` | runs until a limit |
`y y`, `PIPESTATUS` `141 141 0` |
| `yes \| tr y n \| head -1` | runs until a limit | `n`, `141 141 0` |
| `yes \| tr -d '\n' \| head -c 5` | runs until a limit | `yyyyy`, `141
141 0` |
| `grep . big \| head -1; echo ${PIPESTATUS[*]}` | `0 0` | `141 0` |
| `yes \| grep -o -m3 y \| cat` | runs until a limit | 3 lines, `141 0
0` |
Proof: `l_pipe_001_grep_tr_stream` and
`l_pipe_001_buffered_filter_options_drain_pipe` (bash-verified). I also
ran a differential script over grep `-c/-v/-n/-i/-H/-z/-a`, multiple
files, a missing file, binary input and the tr variants. Its only
differences are existing ones outside streaming: stderr ordering,
byte-wise `tr 'é' e`, and `\r` stripping.
## Risk
Medium-low. The buffered paths are unchanged apart from draining a
streaming pipe. `tr`'s logic moved into `TrSpec::apply` unchanged.
Binary input in a stream is reported once with `binary file matches`
from the point the NUL is seen.
## Checklist
- [x] Failing test first
- [x] `cargo test -p bashkit --lib` / `--test integration` /
`bash_comparison`
- [x] clippy `-D warnings`, fmt, `check-okf`, `check-doc-links`
- [x] knowledge: parallel-execution, limitations L-PIPE-001, log
https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
---
_Generated by [Claude
Code](https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA)_
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Михайло · project thread
What changed
cat(no display flags, no redirects) streams through pipelines. As a stage it writes 4 KiB chunks into the stage pipe.catalso reads its input pipe as it goes through a new internalContext::stdin_stream, instead of the interpreter collecting all stdin first.catwith flags drains the input pipe and runs buffered, as before.catnow reports a missing operand and goes on with the rest, as GNU cat does. It used to stop at the first error.Why
Step 2 of the roadmap (streaming builtin I/O).
catis the most common middle stage, and it was the documented example in L-PIPE-001 of a pipeline that never ends.Before / After
seq 20000 > big; cat big | head -1; echo ${PIPESTATUS[*]}1/0 01/141 0while :; do echo y; done | cat | head -2y y,PIPESTATUS141 141 0yes | cat | cat | head -1y,141 141 141 0echo hi | cat - missing 2>&1hiplus the error, status 1New evidence test
l_pipe_001_cat_streams.l_pipe_001_stages_run_sequentiallynow usesgrepas its non-streaming example. L-PIPE-001 is narrowed.Risk
Medium. This touches pipeline stdin handoff, but only for
cat, which is selected by name, unredirected and not shadowed by a function, the same gatingyes/sequse. All lib, integration and bash_comparison tests pass.Checklist
cargo fmt,cargo clippy -D warningshttps://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
Generated by Claude Code