Skip to content

Pipelines drain the shell's shared stdin to EOF — while read …; do … | …; done < file runs only once (regression from #285) #323

Description

@trieloff

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):

a=[L1]
b=[L2]

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions