Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions completions/bun.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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]}" ]] && {
Comment thread
robobun marked this conversation as resolved.
Outdated
local filtered_reply=();
local reply_word script_name keep;
for reply_word in "${COMPREPLY[@]}"; do
Expand Down
4 changes: 2 additions & 2 deletions completions/bun.fish
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions completions/bun.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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]' \
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions test/cli/shell-completion-scripts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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<string> {
await using proc = Bun.spawn({
cmd: [bunExe(), "completions"],
env: { ...bunEnv, SHELL: `/bin/${shell}`, IS_BUN_AUTO_UPDATE: undefined },
stdout: "pipe",
stderr: "pipe",
});

Check failure on line 13 in test/cli/shell-completion-scripts.test.ts

View check run for this annotation

Claude / Claude Code Review

Test is not hermetic: bun completions writes bunx-debug symlinks into real $HOME / $BUN_INSTALL

`emitCompletions` inherits the real `$HOME`/`$BUN_INSTALL` (via `bunEnv`'s `...process.env`), and `bun completions` runs `install_bunx_symlink` **before** the non-TTY write-and-exit branch — so each of the 5 spawns tries to create `bunx-debug` symlinks in `<exe_dir>`, then on `EEXIST` falls through to `$BUN_INSTALL/bin`, `$HOME/.bun/bin`, and `$HOME/.local/bin`, leaving symlinks in the developer's/CI runner's real home with no cleanup. Add `HOME: undefined, BUN_INSTALL: undefined` (or point them
Comment thread
robobun marked this conversation as resolved.
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", () => {
Comment thread
robobun marked this conversation as resolved.
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);
});
});
Loading