SIGN IN SIGN UP

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