Skip to content

feat(interpreter): stream cat through pipelines - #2584

Merged
chaliy merged 1 commit into
mainfrom
claude/stream-filters
Oct 7, 2026
Merged

chaliy merged 1 commit into
mainfrom
claude/stream-filters

Conversation

@chaliy

@chaliy chaliy commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Requested by Михайло · project thread

What changed

  • Plain cat (no display flags, no redirects) streams through pipelines. As a stage it writes 4 KiB chunks into the stage pipe.
  • As a middle stage, cat also reads its input pipe as it goes through a new internal Context::stdin_stream, instead of the interpreter collecting all stdin first.
  • cat with flags drains the input pipe and runs buffered, as before.
  • cat now 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). cat is the most common middle stage, and it was the documented example in L-PIPE-001 of a pipeline that never ends.

Before / After

Script Before After (= bash 5.2)
seq 20000 > big; cat big | head -1; echo ${PIPESTATUS[*]} 1 / 0 0 1 / 141 0
while :; do echo y; done | cat | head -2 ran until the command limit and failed y y, PIPESTATUS 141 141 0
yes | cat | cat | head -1 ran until a limit y, 141 141 141 0
echo hi | cat - missing 2>&1 only the error hi plus the error, status 1

New evidence test l_pipe_001_cat_streams. l_pipe_001_stages_run_sequentially now uses grep as 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 gating yes/seq use. All lib, integration and bash_comparison tests pass.

Checklist

  • Tests (evidence tests, probe script diffed against bash)
  • cargo fmt, cargo clippy -D warnings
  • lib, integration and bash_comparison tests
  • Knowledge updated (parallel-execution, limitations, log)

https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA


Generated by Claude Code

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
@chaliy chaliy self-assigned this Oct 7, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@chaliy
chaliy merged commit 75ca80d into main Oct 7, 2026
44 checks passed
@chaliy
chaliy deleted the claude/stream-filters branch October 7, 2026 18:28
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)_
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.

1 participant