From cff24b2739807e1c8a9d7277b846d7d88f55eaf1 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:26:53 +0000 Subject: [PATCH 1/7] completions: fix 4 correctness bugs in zsh/bash/fish scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zsh: move closing bracket inside the quote for the -i optspec in _bun_run_completion so _arguments sees a single argument (#31665) - zsh: print the history array instead of executing its first element in _bun_add_param_package_completion (#34062) - bash: drop dead OR arm that referenced undeclared re_comp_word_script in _read_scripts_in_package_json (#28744) - fish: add frozen-lockfile to bun_install_boolean_flags and fix the dry-run description so flags and descriptions stay in lockstep (#29364) Adds test/cli/shell-completion-scripts.test.ts which asserts on the embedded scripts that `bun completions` emits to stdout. Co-authored-by: Agusti F. <6601142+agustif@users.noreply.github.com> Co-authored-by: Yahia EL IDRISSI Co-authored-by: Jakub Pečenka Co-authored-by: Lucas Garron --- completions/bun.bash | 5 +- completions/bun.fish | 4 +- completions/bun.zsh | 4 +- test/cli/shell-completion-scripts.test.ts | 81 +++++++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 test/cli/shell-completion-scripts.test.ts diff --git a/completions/bun.bash b/completions/bun.bash index 7040f0ca51b2..3aafc3016768 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -55,10 +55,7 @@ _read_scripts_in_package_json() { # when a script is passed as an option, do not show other scripts as part of the completion anymore local re_prev_script="(^| )${prev}($| )"; - [[ - ( "${COMPREPLY[*]}" =~ ${re_prev_script} && -n "${COMP_WORDS[2]}" ) || \ - ( "${COMPREPLY[*]}" =~ ${re_comp_word_script} ) - ]] && { + [[ "${COMPREPLY[*]}" =~ ${re_prev_script} && -n "${COMP_WORDS[2]}" ]] && { local filtered_reply=(); local reply_word script_name keep; for reply_word in "${COMPREPLY[@]}"; do diff --git a/completions/bun.fish b/completions/bun.fish index 6b2dc5bc9bcd..6d5e7ef255dc 100644 --- a/completions/bun.fish +++ b/completions/bun.fish @@ -32,8 +32,8 @@ function __fish__get_bun_bun_js_files string split ' ' (bun getcompletes j) end -set -l bun_install_boolean_flags yarn production optional development no-save dry-run force no-cache silent verbose global -set -l bun_install_boolean_flags_descriptions "Write a yarn.lock file (yarn v1)" "Don't install devDependencies" "Add dependency to optionalDependencies" "Add dependency to devDependencies" "Don't update package.json or save a lockfile" "Don't install anything" "Always request the latest versions from the registry & reinstall all dependencies" "Ignore manifest cache entirely" "Don't output anything" "Excessively verbose logging" "Use global folder" +set -l bun_install_boolean_flags yarn production optional development no-save frozen-lockfile dry-run force no-cache silent verbose global +set -l bun_install_boolean_flags_descriptions "Write a yarn.lock file (yarn v1)" "Don't install devDependencies" "Add dependency to optionalDependencies" "Add dependency to devDependencies" "Don't update package.json or save a lockfile" "Disallow changes to lockfile" "Perform a dry run without making changes" "Always request the latest versions from the registry & reinstall all dependencies" "Ignore manifest cache entirely" "Don't output anything" "Excessively verbose logging" "Use global folder" set -l bun_builtin_cmds_without_run dev create help bun upgrade discord install remove add update init pm x repl set -l bun_builtin_cmds_accepting_flags create help bun upgrade discord run init link unlink pm x update diff --git a/completions/bun.zsh b/completions/bun.zsh index 9ab15deedf66..9ea31c69a520 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -483,7 +483,7 @@ _bun_run_completion() { '--watch[Automatically restart bun'"'"'s JavaScript runtime on file change]' \ '--no-install[Disable auto install in bun'"'"'s JavaScript runtime]' \ '--install[Install dependencies automatically when no node_modules are present, default: "auto". "force" to ignore node_modules, fallback to install any missing]: :->install_' \ - '-i[Automatically install dependencies and use global cache in bun'"'"'s runtime, equivalent to --install=fallback'] \ + '-i[Automatically install dependencies and use global cache in bun'"'"'s runtime, equivalent to --install=fallback]' \ '--prefer-offline[Skip staleness checks for packages in bun'"'"'s JavaScript runtime and resolve from disk]' \ '--prefer-latest[Use the latest matching versions of packages in bun'"'"'s JavaScript runtime, always checking npm]' \ '--silent[Don'"'"'t repeat the command for bun run]' \ @@ -991,7 +991,7 @@ _set_remove() { _bun_add_param_package_completion() { IFS=$'\n' inexact=($(history -n bun | grep -E "^bun add " | cut -c 9- | uniq)) - IFS=$'\n' exact=($($inexact | grep -E "^$words[$CURRENT]")) + IFS=$'\n' exact=($(print -l -- $inexact | grep -E "^$words[$CURRENT]")) IFS=$'\n' packages=($(SHELL=zsh bun getcompletes a $words[$CURRENT])) to_print=$inexact diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts new file mode 100644 index 000000000000..359d934f9f62 --- /dev/null +++ b/test/cli/shell-completion-scripts.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; + +// `bun completions` writes the embedded completion script for the shell named +// by $SHELL to stdout when stdout is not a TTY. This lets us assert on the +// bytes that ship inside the binary (from completions/bun.{zsh,bash,fish}). +async function emitCompletions(shell: "zsh" | "bash" | "fish"): Promise { + await using proc = Bun.spawn({ + cmd: [bunExe(), "completions"], + env: { ...bunEnv, SHELL: `/bin/${shell}`, IS_BUN_AUTO_UPDATE: undefined }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("error"); + expect(exitCode).toBe(0); + return stdout; +} + +// `bun completions` is a no-op on Windows (PowerShell completions are not +// implemented), so these tests can only run on POSIX. +describe.skipIf(isWindows)("shell completion scripts", () => { + test("zsh: -i optspec has closing bracket inside the quote", async () => { + // #31665: the line was `...=fallback'] \` (bracket outside the quote), + // which zsh _arguments sees as a stray literal `]` argument. + const script = await emitCompletions("zsh"); + expect(script).toContain("--install=fallback]' \\"); + expect(script).not.toContain("--install=fallback'] \\"); + }); + + test("zsh: _bun_add_param_package_completion prints history instead of executing it", async () => { + // #34062: `$($inexact | grep ...)` runs the first history entry as a + // command. It should be `$(print -l -- $inexact | grep ...)`. + const script = await emitCompletions("zsh"); + expect(script).toContain("print -l -- $inexact | grep"); + expect(script).not.toContain("($($inexact | grep"); + }); + + test("bash: no reference to undeclared re_comp_word_script", async () => { + // #28744: ${re_comp_word_script} was never defined; the OR arm expanded + // to `=~ ` which is an empty pattern. + const script = await emitCompletions("bash"); + expect(script).not.toContain("re_comp_word_script"); + }); + + test("bash: script passes bash -n", async () => { + const script = await emitCompletions("bash"); + using dir = tempDir("bun-bash-completion", { "bun.bash": script }); + await using proc = Bun.spawn({ + cmd: ["bash", "-n", "bun.bash"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe(""); + expect(exitCode).toBe(0); + }); + + test("fish: install boolean flags include frozen-lockfile and descriptions line up", async () => { + // #29364: frozen-lockfile was missing and dry-run's description was wrong. + const script = await emitCompletions("fish"); + const flagsLine = script.split("\n").find(l => l.startsWith("set -l bun_install_boolean_flags ")); + const descLine = script + .split("\n") + .find(l => l.startsWith("set -l bun_install_boolean_flags_descriptions ")); + expect(flagsLine).toBeDefined(); + expect(descLine).toBeDefined(); + + const flags = flagsLine!.replace("set -l bun_install_boolean_flags ", "").trim().split(/\s+/); + // Descriptions are quoted with "..." and separated by a single space. + const descs = [...descLine!.matchAll(/"[^"]*"/g)].map(m => m[0]); + + expect(flags).toContain("frozen-lockfile"); + // The two parallel lists must stay in lockstep or every flag after the + // first mismatch gets the wrong help text. + expect(descs.length).toBe(flags.length); + }); +}); From eb60d659b9a37476a797f66798680b1382f664b8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 04:29:18 +0000 Subject: [PATCH 2/7] [autofix.ci] apply automated fixes --- test/cli/shell-completion-scripts.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index 359d934f9f62..ebac3a937221 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -63,9 +63,7 @@ describe.skipIf(isWindows)("shell completion scripts", () => { // #29364: frozen-lockfile was missing and dry-run's description was wrong. const script = await emitCompletions("fish"); const flagsLine = script.split("\n").find(l => l.startsWith("set -l bun_install_boolean_flags ")); - const descLine = script - .split("\n") - .find(l => l.startsWith("set -l bun_install_boolean_flags_descriptions ")); + const descLine = script.split("\n").find(l => l.startsWith("set -l bun_install_boolean_flags_descriptions ")); expect(flagsLine).toBeDefined(); expect(descLine).toBeDefined(); From e4b3ff238d03a6e278be0e195257d7429a1fc09d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 05:01:50 +0000 Subject: [PATCH 3/7] bash: make the script-filter block unconditional instead of dropping the guard The removed OR arm matched against an undeclared variable, which under GNU regex is always-true (empty pattern matches), so the block has always run unconditionally. Dropping only the OR arm turned the guard genuinely conditional and regressed `bun ` on GNU bash from 42 completions to 0. Running the block unconditionally instead preserves the shipped GNU behaviour and still removes the BSD-regex error on macOS. Also: switch tests to test.concurrent and add a functional probe that drives _bun_completions for `bun somefile.js `. --- completions/bun.bash | 28 ++++++------- test/cli/shell-completion-scripts.test.ts | 51 +++++++++++++++++++---- 2 files changed, 57 insertions(+), 22 deletions(-) diff --git a/completions/bun.bash b/completions/bun.bash index 3aafc3016768..d13fccfde3b2 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -53,21 +53,21 @@ _read_scripts_in_package_json() { COMPREPLY+=( $(compgen -W "${package_json_compreply[*]}" -- "${cur_word}") ); } - # when a script is passed as an option, do not show other scripts as part of the completion anymore - local re_prev_script="(^| )${prev}($| )"; - [[ "${COMPREPLY[*]}" =~ ${re_prev_script} && -n "${COMP_WORDS[2]}" ]] && { - local filtered_reply=(); - local reply_word script_name keep; - for reply_word in "${COMPREPLY[@]}"; do - keep=1; - for script_name in "${package_json_compreply[@]}"; do - [[ "${reply_word}" == "${script_name}" ]] && { keep=""; break; }; - done - [[ -n "${keep}" ]] && filtered_reply+=( "${reply_word}" ); + # Do not offer package.json scripts once a script/file has already been typed. This block used + # to be gated on a regex match against an undeclared variable, which made it unconditional under + # GNU regex and an "empty (sub)expression" error under BSD regex (#24847); running it + # unconditionally preserves the long-standing GNU behaviour portably. + local filtered_reply=(); + local reply_word script_name keep; + for reply_word in "${COMPREPLY[@]}"; do + keep=1; + for script_name in "${package_json_compreply[@]}"; do + [[ "${reply_word}" == "${script_name}" ]] && { keep=""; break; }; done - COMPREPLY=( "${filtered_reply[@]}" ); - replaced_script="${prev}"; - } + [[ -n "${keep}" ]] && filtered_reply+=( "${reply_word}" ); + done + COMPREPLY=( "${filtered_reply[@]}" ); + replaced_script="${prev}"; } diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index ebac3a937221..8becb3ff905d 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -20,7 +20,7 @@ async function emitCompletions(shell: "zsh" | "bash" | "fish"): Promise // `bun completions` is a no-op on Windows (PowerShell completions are not // implemented), so these tests can only run on POSIX. describe.skipIf(isWindows)("shell completion scripts", () => { - test("zsh: -i optspec has closing bracket inside the quote", async () => { + test.concurrent("zsh: -i optspec has closing bracket inside the quote", async () => { // #31665: the line was `...=fallback'] \` (bracket outside the quote), // which zsh _arguments sees as a stray literal `]` argument. const script = await emitCompletions("zsh"); @@ -28,7 +28,7 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(script).not.toContain("--install=fallback'] \\"); }); - test("zsh: _bun_add_param_package_completion prints history instead of executing it", async () => { + test.concurrent("zsh: _bun_add_param_package_completion prints history instead of executing it", async () => { // #34062: `$($inexact | grep ...)` runs the first history entry as a // command. It should be `$(print -l -- $inexact | grep ...)`. const script = await emitCompletions("zsh"); @@ -36,14 +36,14 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(script).not.toContain("($($inexact | grep"); }); - test("bash: no reference to undeclared re_comp_word_script", async () => { - // #28744: ${re_comp_word_script} was never defined; the OR arm expanded - // to `=~ ` which is an empty pattern. + test.concurrent("bash: no reference to undeclared re_comp_word_script", async () => { + // #28744 / #24847: ${re_comp_word_script} was never defined; under BSD + // regex (macOS bash) `=~ ` is rejected as "empty (sub)expression". const script = await emitCompletions("bash"); - expect(script).not.toContain("re_comp_word_script"); + expect(script).not.toContain("${re_comp_word_script}"); }); - test("bash: script passes bash -n", async () => { + test.concurrent("bash: script passes bash -n", async () => { const script = await emitCompletions("bash"); using dir = tempDir("bun-bash-completion", { "bun.bash": script }); await using proc = Bun.spawn({ @@ -59,7 +59,42 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(exitCode).toBe(0); }); - test("fish: install boolean flags include frozen-lockfile and descriptions line up", async () => { + test.concurrent("bash: `bun ` still offers global flags", async () => { + // Regression guard for the #24847 fix: the removed `=~ ${re_comp_word_script}` + // guard was always-true under GNU regex, so the `replaced_script` assignment + // it protected has always run. Dropping the arm outright would leave + // `bun somefile.js ` with zero completions instead of the global flag + // list, so the fix makes the block unconditional instead. + const script = await emitCompletions("bash"); + using dir = tempDir("bun-bash-completion-run", { + "bun.bash": script, + "probe.sh": + "source ./bun.bash\n" + + 'COMP_WORDS=(bun somefile.js "")\n' + + "COMP_CWORD=2\n" + + "COMPREPLY=()\n" + + "_bun_completions\n" + + 'echo "count=${#COMPREPLY[@]}"\n' + + 'printf \'%s\\n\' "${COMPREPLY[@]}"\n', + }); + await using proc = Bun.spawn({ + cmd: ["bash", "probe.sh"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const lines = stdout.trim().split("\n"); + expect(lines[0]).toMatch(/^count=\d+$/); + expect(Number(lines[0].slice("count=".length))).toBeGreaterThan(0); + expect(lines).toContain("--help"); + expect(lines).toContain("--version"); + expect(exitCode).toBe(0); + }); + + test.concurrent("fish: install boolean flags include frozen-lockfile and descriptions line up", async () => { // #29364: frozen-lockfile was missing and dry-run's description was wrong. const script = await emitCompletions("fish"); const flagsLine = script.split("\n").find(l => l.startsWith("set -l bun_install_boolean_flags ")); From 9df10cd6184eec93f86efc31b95046dd267e3538 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 05:04:06 +0000 Subject: [PATCH 4/7] [autofix.ci] apply automated fixes --- test/cli/shell-completion-scripts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index 8becb3ff905d..afe37a4638c6 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -75,7 +75,7 @@ describe.skipIf(isWindows)("shell completion scripts", () => { "COMPREPLY=()\n" + "_bun_completions\n" + 'echo "count=${#COMPREPLY[@]}"\n' + - 'printf \'%s\\n\' "${COMPREPLY[@]}"\n', + "printf '%s\\n' \"${COMPREPLY[@]}\"\n", }); await using proc = Bun.spawn({ cmd: ["bash", "probe.sh"], From 26844d16e940d5958bbf41f8f680706ecc7ddd7c Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 05:15:15 +0000 Subject: [PATCH 5/7] test: clear HOME/BUN_INSTALL so bun completions cannot write outside the build tree --- test/cli/shell-completion-scripts.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index afe37a4638c6..0fbd270b5283 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -4,10 +4,18 @@ import { bunEnv, bunExe, isWindows, tempDir } from "harness"; // `bun completions` writes the embedded completion script for the shell named // by $SHELL to stdout when stdout is not a TTY. This lets us assert on the // bytes that ship inside the binary (from completions/bun.{zsh,bash,fish}). +// HOME / BUN_INSTALL are cleared so the `install_bunx_symlink` step that runs +// before the piped-stdout write has nowhere outside the build tree to land. async function emitCompletions(shell: "zsh" | "bash" | "fish"): Promise { await using proc = Bun.spawn({ cmd: [bunExe(), "completions"], - env: { ...bunEnv, SHELL: `/bin/${shell}`, IS_BUN_AUTO_UPDATE: undefined }, + env: { + ...bunEnv, + SHELL: `/bin/${shell}`, + IS_BUN_AUTO_UPDATE: undefined, + HOME: undefined, + BUN_INSTALL: undefined, + }, stdout: "pipe", stderr: "pipe", }); From 205ef2431ad6678c280920da1d66d276fa316f7b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 05:20:19 +0000 Subject: [PATCH 6/7] test: gate functional bash probe on bash>=4; assert fish flag/desc positional pairs --- test/cli/shell-completion-scripts.test.ts | 41 +++++++++++++++++------ 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index 0fbd270b5283..1ea60de5c54e 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -25,6 +25,19 @@ async function emitCompletions(shell: "zsh" | "bash" | "fish"): Promise return stdout; } +// completions/bun.bash uses `declare -A` (associative arrays, bash >= 4.0). +// macOS ships /bin/bash 3.2, so the functional probe can only run where a +// modern bash is on PATH. +const bashMajor = (() => { + if (isWindows) return 0; + try { + const { stdout } = Bun.spawnSync({ cmd: ["bash", "-c", "echo ${BASH_VERSINFO[0]}"], env: bunEnv }); + return parseInt(stdout.toString().trim(), 10) || 0; + } catch { + return 0; + } +})(); + // `bun completions` is a no-op on Windows (PowerShell completions are not // implemented), so these tests can only run on POSIX. describe.skipIf(isWindows)("shell completion scripts", () => { @@ -67,12 +80,14 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(exitCode).toBe(0); }); - test.concurrent("bash: `bun ` still offers global flags", async () => { - // Regression guard for the #24847 fix: the removed `=~ ${re_comp_word_script}` - // guard was always-true under GNU regex, so the `replaced_script` assignment - // it protected has always run. Dropping the arm outright would leave - // `bun somefile.js ` with zero completions instead of the global flag - // list, so the fix makes the block unconditional instead. + // Drives _bun_completions directly. Skipped when PATH bash is < 4 (macOS + // stock /bin/bash is 3.2 and rejects the script's `declare -A`). + test.concurrent.skipIf(bashMajor < 4)("bash: `bun ` still offers global flags", async () => { + // Regression guard for the #24847 fix: the removed guard was always-true + // under GNU regex, so the `replaced_script` assignment it protected has + // always run. Dropping the arm outright would leave `bun somefile.js ` + // with zero completions instead of the global flag list, so the fix makes + // the block unconditional instead. const script = await emitCompletions("bash"); using dir = tempDir("bun-bash-completion-run", { "bun.bash": script, @@ -111,12 +126,16 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(descLine).toBeDefined(); const flags = flagsLine!.replace("set -l bun_install_boolean_flags ", "").trim().split(/\s+/); - // Descriptions are quoted with "..." and separated by a single space. - const descs = [...descLine!.matchAll(/"[^"]*"/g)].map(m => m[0]); + // Descriptions are a flat sequence of "..." literals separated by spaces. + const descs = [...descLine!.matchAll(/"([^"]*)"/g)].map(m => m[1]); - expect(flags).toContain("frozen-lockfile"); - // The two parallel lists must stay in lockstep or every flag after the - // first mismatch gets the wrong help text. + // The two parallel lists are zipped by index at runtime; length equality + // alone cannot catch an insertion at the wrong position, so spot-check + // the pairings this change touched as well. expect(descs.length).toBe(flags.length); + expect(flags).toContain("frozen-lockfile"); + expect(descs[flags.indexOf("frozen-lockfile")]).toContain("lockfile"); + expect(descs[flags.indexOf("dry-run")]).toMatch(/dry run/i); + expect(descs[flags.indexOf("global")]).toMatch(/global/i); }); }); From 66b7669bc181937b897417f720272b96ec761e57 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 31 Jul 2026 06:04:21 +0000 Subject: [PATCH 7/7] test: skip bash -n test when bash is absent from PATH --- test/cli/shell-completion-scripts.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cli/shell-completion-scripts.test.ts b/test/cli/shell-completion-scripts.test.ts index 1ea60de5c54e..ca664c3397b3 100644 --- a/test/cli/shell-completion-scripts.test.ts +++ b/test/cli/shell-completion-scripts.test.ts @@ -64,7 +64,7 @@ describe.skipIf(isWindows)("shell completion scripts", () => { expect(script).not.toContain("${re_comp_word_script}"); }); - test.concurrent("bash: script passes bash -n", async () => { + test.concurrent.skipIf(bashMajor < 1)("bash: script passes bash -n", async () => { const script = await emitCompletions("bash"); using dir = tempDir("bun-bash-completion", { "bun.bash": script }); await using proc = Bun.spawn({