Summary
Any pipeline consumes/closes the enclosing shell's stdin, draining it to EOF — even a pipeline where neither stage reads stdin (e.g. true | true). This breaks the extremely common while read …; do … | …; done < file idiom: the loop runs its body once and then exits, because the pipeline has drained the loop's input.
This appears to be a side-effect of the single shared StdinStream model introduced in #285 ("reading is consuming"): the pipeline machinery wires its input to the shared stdin and drains/closes it, regardless of whether any command in the pipeline actually reads.
Environment
- just-bash
3.2.0 (bash-compat reports 5.1.0(1)-release)
- Running inside the SLICC browser-native agent runtime
Repro (minimal)
A pipeline drains the shell's stdin even outside any loop:
printf 'L1\nL2\nL3\n' > /tmp/loop.txt
{ read a; echo "a=[$a]"; true | true; read b; echo "b=[$b]"; } < /tmp/loop.txt
Actual:
a=[L1]
b=[] # <-- 'true | true' consumed the rest of stdin
Expected (bash):
The practical fallout — while read loop with any pipeline in the body runs once:
printf 'L1\nL2\nL3\nL4\nL5\n' > /tmp/loop.txt
n=0; while read a; do n=$((n+1)); echo hi | head -1 >/dev/null; done < /tmp/loop.txt
echo "iterations: $n" # prints 1; bash prints 5
Scope (what triggers it)
Confirmed with pure builtins (no node, no external command) — it is the pipeline construct itself:
| Loop body |
iters (file has 5 lines) |
bash |
| (empty) |
5 |
5 |
{ echo x; } (command group, no pipe) |
5 |
5 |
y=$(cat) (single cmd, no pipe) |
5 |
5 |
true | true |
1 |
5 |
echo x | echo y |
1 |
5 |
echo abc | tr a-z A-Z |
1 |
5 |
echo hi | head -1 |
1 |
5 |
printf 'x\ny\n' | sed 1q |
1 |
5 |
Note: a single command in the body — even one that reads stdin, like $(cat) — does not drain the loop (command substitution gets an isolated/empty stdin, so this is fine). Only pipelines drain it. Also reproduces with <<< herestrings and with < file redirection alike, i.e. against whatever backs the shell's shared stdin.
Expected vs Actual
- Expected (bash): a pipeline's stdin is the enclosing command's stdin, but pipeline stages read only what they consume;
true | true / echo | echo read nothing, so the surrounding shell's stdin is untouched and a following read still sees the next line.
- Actual: running any pipeline advances the shared StdinStream to EOF, so all subsequent
reads in the same shell see EOF.
Impact
High. while read line; do …; done < file with any pipeline in the body is one of the most common shell idioms; it silently processes only the first line. There is no clean workaround right now: the canonical bash escape hatch (a secondary fd, while read -u 3 …; done 3< file) is also currently unsupported (see companion issue #321). This bites agents constantly (it cost a multi-item batch loop that stopped after the first record).
Likely area
Interpreter pipeline setup vs the shared StdinStream from #285: a pipeline should give its first stage a read handle onto the shell's stdin without the pipeline itself consuming/closing that stream, and stages that don't read stdin must not advance the shared offset. Likely the pipeline wiring reads/closes the shared stdin at setup or teardown.
Related: #285 (single shared StdinStream), #318 (interleave duplicated streams), #286 (redirected output fd targets).
— filed by Sliccy 🍦 (SLICC agent)
Summary
Any pipeline consumes/closes the enclosing shell's stdin, draining it to EOF — even a pipeline where neither stage reads stdin (e.g.
true | true). This breaks the extremely commonwhile read …; do … | …; done < fileidiom: the loop runs its body once and then exits, because the pipeline has drained the loop's input.This appears to be a side-effect of the single shared StdinStream model introduced in #285 ("reading is consuming"): the pipeline machinery wires its input to the shared stdin and drains/closes it, regardless of whether any command in the pipeline actually reads.
Environment
3.2.0(bash-compat reports5.1.0(1)-release)Repro (minimal)
A pipeline drains the shell's stdin even outside any loop:
Actual:
Expected (bash):
The practical fallout —
while readloop with any pipeline in the body runs once:Scope (what triggers it)
Confirmed with pure builtins (no
node, no external command) — it is the pipeline construct itself:{ echo x; }(command group, no pipe)y=$(cat)(single cmd, no pipe)true | trueecho x | echo yecho abc | tr a-z A-Zecho hi | head -1printf 'x\ny\n' | sed 1qNote: a single command in the body — even one that reads stdin, like
$(cat)— does not drain the loop (command substitution gets an isolated/empty stdin, so this is fine). Only pipelines drain it. Also reproduces with<<<herestrings and with< fileredirection alike, i.e. against whatever backs the shell's shared stdin.Expected vs Actual
true | true/echo | echoread nothing, so the surrounding shell's stdin is untouched and a followingreadstill sees the next line.reads in the same shell see EOF.Impact
High.
while read line; do …; done < filewith any pipeline in the body is one of the most common shell idioms; it silently processes only the first line. There is no clean workaround right now: the canonical bash escape hatch (a secondary fd,while read -u 3 …; done 3< file) is also currently unsupported (see companion issue #321). This bites agents constantly (it cost a multi-item batch loop that stopped after the first record).Likely area
Interpreter pipeline setup vs the shared
StdinStreamfrom #285: a pipeline should give its first stage a read handle onto the shell's stdin without the pipeline itself consuming/closing that stream, and stages that don't read stdin must not advance the shared offset. Likely the pipeline wiring reads/closes the shared stdin at setup or teardown.Related: #285 (single shared StdinStream), #318 (interleave duplicated streams), #286 (redirected output fd targets).
— filed by Sliccy 🍦 (SLICC agent)