feat(parser,interpreter): support process substitution <(…) and >(…) (#325)
`cat <(echo hi)` failed to parse: the lexer read `<` as an input redirection and the parser then found `(` where a filename belonged. Process substitution is a word-level construct, so `<(` / `>(` are now folded into the word token when the paren is immediately adjacent, and `parseWordParts` turns them into the (previously unused) `ProcessSubstitution` AST node. `< (cmd)`, `<<`, `<<<` and `>` inside `(( ))` are unaffected. At runtime `<(cmd)` runs the body as a subshell during word expansion, writes its stdout to a `/dev/fd/N` file in the VFS and substitutes that path; `>(cmd)` substitutes an empty writable path and feeds whatever the outer command wrote to the body once that command finishes. Descriptor numbers count down from 63 like bash and are released when the command that opened them completes, so nothing accumulates across commands. Read-only sandboxes (the `just-bash` CLI's default) reject every write, including the outer command's write into a `>(cmd)` path. Real bash runs process substitution fine on a read-only mount, so the first refused backing-file write routes `/dev/fd` to a private filesystem via `MountableFs`, wrapping rather than mutating the caller's filesystem. That mount is not scratch space: it gates writes on the interpreter's live-descriptor set, so every other path under `/dev/fd` still raises the `EROFS` the base filesystem would, and a read-only sandbox behaves the same whether or not a substitution ran first. Body failures never reach `$?` and body output is bounded by the same limits command substitution uses. Closes #320 Signed-off-by: Lars Trieloff <lars@trieloff.net>
L
Lars Trieloff committed
edc7f2fac5337cebd19911c5756b76ed02e52090
Parent: 74145d0
Committed by GitHub <noreply@github.com>
on 8/6/2026, 2:03:33 AM