feat(interpreter): expose stdinConnected on the command context - #448
mutewinter wants to merge 3 commits into
Conversation
A custom command received ctx.stdin as bytes and nothing about where they came from, so `false | cmd` and a bare `cmd` arrived identically. A command that reads stdin only when it has one (ripgrep, which walks the directory otherwise) had to guess from the byte count, and a producer that printed nothing became a directory walk. ctx.stdinConnected says whether a pipe, a redirection, or an enclosing group's stdin is on the other end of fd 0, independent of whether bytes arrived. Underneath, every pipeline stage after the first now owns its stdin the way a redirection from an empty file does, so the flag holds inside a group, subshell, or function that a pipe feeds.
|
@mutewinter is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
🤖 auto-maintain reviewAutomated, advisory triage for
Review panel: 🟡 medium highest severity just-bash maintainer code review: 🟡 medium
General code review: 🟡 medium
Adversarial security: 🟡 medium
Adversarial security (second opinion): 🟢 low
Standard Bash and host portability: 🟡 medium
Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call. |
…script, and nested shell A pipeline stage after the first owned its stdin only when it was a simple command, group, subshell, or function. `if`, `for`, C-style `for`, `while`, `until`, and `case` dropped the ownership bit, and the first three dropped the bytes as well, so `printf 'a\n' | if true; then read x; fi` left `x` empty where bash reads `a`. One helper now installs the owned stream for all six, replacing the loop-only version. An executable script installs the stream it was handed even when it is empty, so a custom command inside `printf '' | ./script` sees fd 0 connected. `bash -c` forwards its stdin to the nested shell only when its own fd 0 is connected, so a bare `bash -c cmd` no longer hands every command inside it an empty stream to report. Connectedness is now computed beside ownership rather than derived from it: `cmd 0<&-` owns a closed fd 0 and reports false, and `cmd <&0` inherits the answer instead of forcing true. The value travels through `command`, `builtin`, and `exec` to the command they wrap. Comparison fixtures recorded against GNU bash 3.2.57 pin the compound-command byte behavior; seven of nine fail without this change.
|
All of it held, and the second commit takes each finding, with a test per case. Compound commands. Executable scripts.
One thing I did not change, and the body now says so: a closed fd 0 across a group or script boundary ( I also found #401 after opening this, which fixes the same symptom for the bundled Unit 14,841 pass, comparison 881, spec 3,834; |
…, and give source the stdin it was piped
A scope given a closed fd 0 by its caller (`{ cmd; } 0<&-`, `f 0<&-`, `eval … 0<&-`, `./script 0<&-`) installed an owned empty stream, and every command inside then reported fd 0 as connected. Closedness now travels beside ownership: `groupStdinClosed` beside `groupStdin` in the interpreter state, saved and restored at every install site, and a `stdinClosed` parameter beside `stdinOwned` on the command executors, functions, eval, source, and user scripts. A scope's own open redirection still wins over a closed fd 0 from its caller, as in bash; a scope's own `0<&-` closes it for everything inside.
`source` ran the file with no stdin at all, so `printf 'a\n' | source file` left a `read` inside empty where bash reads `a`. It now owns the stream it was piped or redirected the way `eval` does, restoring the enclosing one afterwards, installed only once the depth check has passed.
Comparison fixtures recorded against GNU bash 3.2.57 pin the sourced file reading the pipe and its reads advancing the shared position.
|
Both findings on the second round held, and the third commit takes them. A closed fd 0 across a boundary. I had scoped this out and the review was right not to let it go:
Unit 14,857 pass, comparison 883, spec 3,834; |
Problem
A custom command receives
ctx.stdinas bytes and nothing about where they came from, so an empty pipe and no pipe are indistinguishable:A host tells them apart by
fstaton fd 0. We embed just-bash and shell the realrg; guessing from the byte count meantfalse | rg PATTERNwalked the task directory and returned its files as if they had been piped. It bit an agent: a failed producer upstream ofrgturned into a 118KB dump of unrelated files presented as pipe content.#401 fixes the same symptom for the bundled
rgby carrying an@internalstdinProvidedto a directly dispatched command. This one is the public form of that signal, for a custom command, and it holds through the compound commands, scripts, and nested shells a pipe can feed. Happy to rebase onto #401 if it lands first; the two overlap inpipeline-execution.ts,builtin-dispatch.ts, andtypes.ts.Cause
RuntimeCommandContextcarriesstdinand no connectedness signal. The interpreter already knows it (stdinRedirectedfor a redirect; a pipeline hands every stage after the first the previous stage's stdout, empty or not) but drops it at the command boundary.Underneath that, a pipeline stage after the first owned its stdin only when it was a simple command, group, subshell, or function.
if,for, andcasereceived no stdin at all, and so didsource:printf 'a\n' | if true; then read x; fiandprintf 'a\n' | source fileleftxempty where bash readsa.Fix
Add
ctx.stdinConnected: true when a redirect, a pipe, or an enclosing group's stdin is on fd 0, independent of whether any bytes arrived; false for a closed fd 0 (cmd 0<&-), and inherited rather than forced bycmd <&0. A scope carries closedness alongside ownership (groupStdinClosedbesidegroupStdin, and astdinClosedparameter besidestdinOwned), so{ cmd; } 0<&-,f 0<&-,eval … 0<&-,source file 0<&-, and./script 0<&-read as EOF inside, as before, and reportfalse; a scope's own open redirection (f() { cmd; } < file; f 0<&-) wins, as in bash.Underneath, every pipeline stage after the first owns its stdin the way a redirect from an empty file does, and one helper installs that stream for all six compound commands, replacing the loop-only version. So the flag holds inside a group, subshell, function,
if,for,while,until,case, executable script, sourced file,eval, orbash -cthat a pipe feeds, andif,for,case, andsourcenow read the pipe's bytes as bash does.bash -cforwards its stdin to the nested shell only when its own fd 0 is connected, so a barebash -c cmdno longer hands every command inside it an empty stream to report. The value travels throughcommand,builtin, andexecto the command they wrap.Scope
Unchanged:
ctx.stdinand every existing consumer; whateval, a function, andcommandown (a closed fd 0 still reads as EOF inside them, as before). Not addressed, deliberately:isatty-style distinctions beyond connectedness, since no in-tree command needs them.Tests
custom-commands.test.ts:ctx.stdinConnectedfor a bare command, an empty pipe, a failed producer, an empty middle stage, a redirect from an empty file, an empty here-string, a pipe into each of group, subshell, function,if,for, C-stylefor,while,until,case, executable script,bash -c,command,exec,eval, andsource, and a closed fd 0 reopened by an inner or a function's own redirection; false for a barebash -c, a closed fd 0,cmd <&0with nothing behind it, a script and anifwith nothing to hand on, afterexec 0<&-, and a closed fd 0 on a group, subshell, function call, function definition,eval,source, executable script,if,while, two scopes up, and inside a pipe, with the flag true again once that scope ends; reset once the pipeline is over; and the standalone-context path. Ten of the cases fail on the first commit of this PR and twelve more on the second.pipeline-compound-stdin.comparison.test.ts: eleven fixtures recorded against GNU bash 3.2.57 forif,else,for, C-stylefor, andcasebodies and a sourced file reading a pipe, a compound command's own redirect winning over the pipe, the pipe not reaching past the compound command, and a sourced file's reads advancing the shared position; seven fail without the second commit and one more without the third.Unit suite 14,857 pass; comparison 883; spec 3,834. The three
*.bundle.test.tsfiles (34 cases) fail identically without this change because they require a builtdist/bundle/; verified by running the same files on the first commit in a separate worktree and diffing the case list, not inferred.Authored with Claude Opus 5