Repository navigation
shell: reject !, brace groups, here-documents and stray then/elif/else/fi at parse time - #43922
Open
robobun wants to merge 2 commits into
Conversation
robobun
force-pushed
the
robobun/7610aab4/shell-reject-reserved-words
branch
from
September 24, 2026 21:07
fe49fb0 to
4a9aa17
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 24, 2026 21:07
61e9254 to
fbb1dae
Compare
Collaborator
Author
|
Reproduced on 1.4.3-canary.1 and on the base branch (Linux x64): import { $ } from "bun";
const r = await $`if ! false; then echo THEN; else echo ELSE; fi`.nothrow().quiet();
console.log(JSON.stringify(r.stdout.toString()), r.exitCode);
PR: #43922. It is stacked on #43921. #43861 is stacked on it. |
Collaborator
Author
|
Updated 8:51 PM PT - Sep 24th, 2026
✅ @robobun, your commit 761e8c8202fca3cabd9fa97d6abfb68c72950767 passed in 🧪 To try this PR locally: bunx bun-pr 43922That installs a local version of the PR into your bun-43922 --bun |
robobun
force-pushed
the
robobun/7610aab4/shell-reject-reserved-words
branch
from
September 24, 2026 22:25
4a9aa17 to
b3f2624
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 24, 2026 22:25
fbb1dae to
b29d9fc
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-reserved-words
branch
from
September 24, 2026 22:37
b3f2624 to
fe3bfa6
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 24, 2026 22:37
b29d9fc to
440ca75
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-reserved-words
branch
from
September 25, 2026 00:57
fe3bfa6 to
ca7e023
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 25, 2026 00:57
440ca75 to
70b1dac
Compare
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 25, 2026 02:39
70b1dac to
2e45e47
Compare
…y then/elif/else/fi
Bun Shell does not implement pipeline negation, brace groups or
here-documents. Each one ran as plain commands: `if ! cmd` took the wrong
branch, the body of `guard && { ...; }` ran when the guard failed, and the
lines of a here-document body ran as commands. A then/elif/else/fi with no
open if ran as a command that does not exist, and the script continued.
parse_compound_cmd now fails the parse for `!`, for a delimited `{` or `}`
in command position, and for a word of the if family that no if clause
consumed. It reads the reserved-word table once for the if family and for
the unsupported words. parse_redirect fails the parse for `<<`.
A backslash-escaped word of the if family is a plain word in every
position, as an escaped unsupported word already is.
The lexer reads a digit before `<` as a file descriptor only for fd 0.
`0< file` had the flags of `<<`. `1< file` and `2< file` had the flags of
`1>> file` and `2>> file`, so they wrote to the file.
robobun
force-pushed
the
robobun/7610aab4/shell-reject-bang-braces-heredoc
branch
from
September 25, 2026 03:02
2e45e47 to
cee8742
Compare
This was referenced Sep 30, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12602
Part of #43860. Related to #10465. Stacked on #43921.
Problem
!, brace groups or here-documents (<<).if ! false; then echo THEN; else echo ELSE; fiprintsELSE. The body offalse && {⏎ echo BODY⏎}runs. Here-document lines run as commands.then,elif,elseorfiruns as a missing command.Parser::parse_compound_cmd(src/shell_parser/parse.rs).parse_redirectreads<<as<.Fix
!joins the reserved-word table. A delimited{or}gets the same error. A stray if-family word fails withUnexpected token: `fi`. An escaped word (\fi) stays plain.parse_redirectfails the parse for<<. The lexer reads a digit before<as a file descriptor only for fd 0 (Bun shell parses some redirections incorrectly #12602).test/js/bun/shell/bunshell.test.ts(25 new cases fail on the base branch),exec.test.ts,test/cli/run/run-shell.test.ts.Background
{a,b}) also starts withBraceBegin. The new arm needs a delimiter after the brace, so{a,b}stays.parse_if_clauseconsumes each if-family word of anif.<<, which is a redirect.Downsides
ok || { …; }exited 0 whenokpassed.\ifno longer opens anif.cmd << fileno longer readsfile. Incmd 1< file,1is now an argument.Notes
Measurements. Release builds of the base branch (ca7e023) and of this PR. Instruction counts from gdb single-stepping over a window on
bun-profile(entry of a function to its return, second call).valgrind,perfandstraceare not available in the build container.createParsedShellScript,echo a; echo b | cat; true && echo c(4 commands)parse_compound_cmdcall,echo aparse_redirectcall with a redirectInterpreter::parse, 64 commandsecho x > /dev/nullshell_cmd_from_js, 8 interpolated values (3-char, 4-char, UTF-16, 12-char)$.escape("hello")createParsedShellScript, the 4-command scriptInterpreter::parse, K commands with M by-reference values, K=M=100 / 200 / 400.text+.rodata(size -A)parse_compound_cmdreads the reserved-word table once per command. The base branch reads it twice: once forifand once for the unsupported words.reserved_word_atlooks the word up first. It scans the interpolated ranges and the escaped positions only on a hit. main scans the interpolated ranges for each command, so its cost grows with commands times values (2,048,084 at K=M=400). The K=200 and K=400 windows contain 1 and 3 vDSOclock_gettimecalls, which run at full speed and are not counted. Two measurements of such a window differ by 6 to 18 instructions. Each other window repeats exactly.Before and after, on the base branch and on this PR:
if ! false; then echo THEN; else echo ELSE; fiELSE, exit 0"!" is a reserved word that Bun Shell does not support yet. …false && {⏎ echo IN_GROUP⏎}⏎echo tailIN_GROUP,tail"{" is a reserved word …cat <<-EOF⏎<TAB>echo BODY_LINE_RAN⏎<TAB>EOF⏎echo tailNo such file or directory: -EOF, then each body line runsHere-documents "<<" are not supported yet.cat 1<<EOF⏎echo BODY_LINE_RAN⏎EOFRedirection with no fileHere-documents "<<" are not supported yet.echo a; fi; echo ba,b, exit 0Unexpected token: `fi`if true; then⏎fiExpected "else", "elif", or "fi" but got: EofUnexpected token: `fi`cat 0< input.txtecho hi 1< notes.txthitonotes.txt, prints nothinghi 1, the file stays as it isecho hi 2< notes.txthihi 2\fi,f\i,\thenbun: command not found: fi(orthen)A digit before
<(#12602).eat_redirectpeeked the<after the digit and did not eat it, soeat_simple_redirect_operatorcounted the same<as a second one and set APPEND.0< filehad the flags of<<, and1< filehad the flags of1>> file. The lexer now eats the<and accepts only fd 0 in front of it. For1<and2<the digit joins the word and<is a plain redirect of stdin. That is what #12602 asks for and what3<already did. The here-document check needs this change: without itparse_redirectcannot tell0< filefrom<<. The first version of this change ate the<for every digit. Then1< filehad the flags of1> fileand truncated the file. The review of this PR found it../script1<file(the script of #12602)./script1command not found: ./script./script1echo hi1< notes.txthi1hitonotes.txthi1echo hi 1< notes.txtwrite error: Bad file descriptorhitonotes.txthi 1echo hi 2< notes.txthihihi 2echo hi 3< notes.txthihi 3hi 3bash reads a digit as a file descriptor only when the digit is a word of its own. Bun Shell has no read redirect for a file descriptor other than 0, so the last three rows differ from bash. The tests pin the output of each row and that the file stays as it is. A comment in the test marks the rows that differ from bash. #27257 and #33997 (both closed without a merge) had the word-boundary rule for
<and>.An escaped if-family word, checked against bash 5.2. The lexer records the position of each char that a backslash escaped (#43921).
reserved_word_atreads that record, and every if-family check passes through it.\fifi: command not found\if true; then echo yes; fithenyesUnexpected token: `then`if true; then echo yes; \fiyesExpected "else", "elif", or "fi" but got: Eofif true; \then echo yes; fifiyesUnexpected token: `fi`Who can see the behaviour change. A script whose construct sits on a branch that does not run.
true || { echo failed; exit 1; }; echo afterprintedafterand exited 0 before, withbun: command not found: }on stderr. With a failing guard (false || { echo warn; }) the fallback did not run and the script still exited 0. Both forms now fail the parse.package.jsonscripts and the lifecycle scripts ofbun installrun under Bun Shell by default on Windows, so a script of this shape fails there now. A script that escapes a word of anif(\if,\then,\fi) parsed as anifbefore and fails the parse now. No user reported!, a brace group or a stray if-family word. A read of the parser for #43860 found them. #10465 asks for here-documents. This PR does not add them: it replaces the run of the body lines with an error.Self-review. A direct read raised eight concerns, all addressed: one lookup for the if family and the unsupported words must not route a word into the
panic!ofexpect_if_clause_text_token(each call site follows a check of the same token, and four new tests pin glued words such asfi$x), the order of the lookup and the delimiter check (lookup first),\ifchanges for scripts that main accepted (stated, matches bash), escaped words inside$( ), backticks and subshells (16 scripts checked against bash),IfClauseTok::from_texthad no caller left (deleted), a brace group on a branch that does not run (stated above),<<<gets the here-document error (stated below), and the changed message for an empty then-body (in the table above). A longer review raised five more. Addressed: the body did not link #12602 and #10465, the body did not say that nobody reported!or brace groups, the test pinnedhi 1andhi 2with no note that bash gives other output (now a comment in the test), and the base branch needs a maintainer decision first (this PR is stacked on it). Rejected: a separate PR for the lexer change, because the here-document check depends on it.Not in this PR.
<<<(here-string) gets the here-document error, because the lexer reads it as<<and<.>and before0<:echo a2> out.txtprintsaand redirects stderr. bash writesa2to the file. main does the same.no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/shell/bunshell.test.ts, test/cli/run/run-shell.test.ts