diff --git a/completions/bun.bash b/completions/bun.bash index 29304d7e69ca..d3c3bec5beba 100644 --- a/completions/bun.bash +++ b/completions/bun.bash @@ -2,16 +2,13 @@ _file_arguments() { local extensions="${1}" - local reset=$(shopt -p globstar) - shopt -s globstar + local reset="$(shopt -p extglob 2>/dev/null)" + shopt -s extglob 2>/dev/null - if [[ -z "${cur_word}" ]]; then - COMPREPLY=( $(compgen -fG -X "${extensions}" -- "${cur_word}") ); - else - COMPREPLY=( $(compgen -f -X "${extensions}" -- "${cur_word}") ); - fi + COMPREPLY+=( $(compgen -d -- "${cur_word}") ) + COMPREPLY+=( $(compgen -f -X "${extensions}" -- "${cur_word}") ) - $reset + eval "${reset}" 2>/dev/null } _long_short_completion() { @@ -69,17 +66,6 @@ _read_scripts_in_package_json() { [[ -n "${keep}" ]] && filtered_reply+=( "${reply_word}" ); done COMPREPLY=( "${filtered_reply[@]}" ); - replaced_script="${prev}"; - } -} - - -_subcommand_comp_reply() { - local cur_word="${1}" - local sub_commands="${2}" - local regexp_subcommand="^[dbcriauh]"; - [[ "${prev}" =~ ${regexp_subcommand} ]] && { - COMPREPLY+=( $(compgen -W "${sub_commands}" -- "${cur_word}") ); } } @@ -90,9 +76,10 @@ _bun_completions() { declare -A PM_OPTIONS; local SUBCOMMANDS="dev bun create run install add remove upgrade completions discord help init pm x test repl update audit dedupe prune outdated link unlink build"; + local RUN_EXTS="!(*.@(js|mjs|cjs|ts|mts|cts|jsx|tsx|wasm|html)?($|))"; - GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js"; - GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p"; + GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --env-file --extension-order --jsx-factory --jsx-fragment --extension-order --jsx-factory --jsx-fragment --jsx-import-source --jsx-production --jsx-runtime --main-fields --no-summary --version --platform --public-dir --tsconfig-override --define --external --help --inject --loader --origin --port --dump-environment-variables --dump-limits --disable-bun-js --hot --watch --no-clear-screen --smol --bun --silent --preload --require --import --inspect --inspect-brk --inspect-wait"; + GLOBAL_OPTIONS[SHORT_OPTIONS]="-c -v -d -e -h -i -l -u -p -b -r"; PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional --peer --catalog --filter"; PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d -F"; @@ -116,13 +103,26 @@ _bun_completions() { local cur_word="${COMP_WORDS[${COMP_CWORD}]}"; local prev="${COMP_WORDS[$(( COMP_CWORD - 1 ))]}"; + # Locate the first non-option word so that runtime flags before the + # subcommand (e.g. `bun --hot run …`, `bun --watch …`) don't break dispatch. + local first_word="" i; + for (( i=1; i < COMP_CWORD; i++ )); do + case "${COMP_WORDS[i]}" in + =|:) ((i++));; + --conditions|--config|--console-depth|--cwd|--define|--dns-result-order|--elide-lines|--env-file|--eval|--extension-order|--fetch-preconnect|--filter|--import|--install|--jsx-factory|--jsx-fragment|--jsx-import-source|--jsx-runtime|--loader|--main-fields|--max-http-header-size|--origin|--port|--preload|--print|--public-dir|--require|--shell|--target|--title|--tsconfig-override|--unhandled-rejections|-F|-c|-d|-e|-l|-p|-r|-u) + ((i++)); [[ "${COMP_WORDS[i]}" == "=" || "${COMP_WORDS[i]}" == ":" ]] && ((i++));; + -*) ;; + *) first_word="${COMP_WORDS[i]}"; break;; + esac + done + case "${prev}" in help|--help|-h|-v|--version) return;; -c|--config) _file_arguments "!*.toml" && return;; --bunfile) _file_arguments "!*.bun" && return;; --server-bunfile) _file_arguments "!*.server.bun" && return;; --backend) - case "${COMP_WORDS[1]}" in + case "${first_word}" in a|add|remove|rm|install|i|dedupe|update|up) COMPREPLY=( $(compgen -W "clonefile copyfile hardlink clonefile_each_dir symlink" -- "${cur_word}") ); ;; @@ -132,7 +132,7 @@ _bun_completions() { COMPREPLY=( $(compgen -W "dev optional peer" -- "${cur_word}") ); return;; -F|--filter) - case "${COMP_WORDS[1]}" in + case "${first_word}" in a|add|remove|rm|i|install|prune|update|up) return;; esac ;; @@ -156,8 +156,8 @@ _bun_completions() { return;; esac - case "${COMP_WORDS[1]}" in - help|completions|--help|-h|-v|--version) return;; + case "${first_word}" in + help|completions) return;; add|a) _long_short_completion \ "${PACKAGE_OPTIONS[ADD_OPTIONS_LONG]} ${PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_LONG]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_SHORT]}" \ @@ -201,45 +201,31 @@ _bun_completions() { COMPREPLY=( $(compgen -W "--help -h --eval -e --print -p --preload -r --smol --config -c --cwd --env-file --no-env-file" -- "${cur_word}") ); return;; run) - _file_arguments "!(*.@(js|ts|jsx|tsx|mjs|cjs)?($|))"; - COMPREPLY+=( $(compgen -W "--version --cwd --help --silent -v -h" -- "${cur_word}" ) ); _read_scripts_in_package_json; + COMPREPLY+=( $(compgen -W "--version --cwd --help --silent -v -h" -- "${cur_word}" ) ); + _file_arguments "${RUN_EXTS}"; return;; pm) _long_short_completion \ "${PM_OPTIONS[LONG_OPTIONS]} ${PM_OPTIONS[SHORT_OPTIONS]}"; COMPREPLY+=( $(compgen -W "bin ls licenses cache hash hash-print hash-string" -- "${cur_word}") ); return;; - *) - local replaced_script; + "") _long_short_completion \ "${GLOBAL_OPTIONS[*]}" \ "${GLOBAL_OPTIONS[SHORT_OPTIONS]}" - - _read_scripts_in_package_json; - _subcommand_comp_reply "${cur_word}" "${SUBCOMMANDS}"; - - # determine if completion should be continued - # when the current word is an empty string - # the previous word is not part of the allowed completion - # the previous word is not an argument to the last two option - [[ -z "${cur_word}" ]] && { - local prev_in_reply=""; - local reply_word; - for reply_word in "${COMPREPLY[@]}"; do - [[ "${reply_word}" == "${prev}" ]] && { prev_in_reply=1; break; }; - done - [[ -z "${prev_in_reply}" ]] && { - local re_prev_prev="(^| )${COMP_WORDS[(( COMP_CWORD - 2 ))]}($| )"; - local global_option_with_extra_args="--bunfile --server-bunfile --config --port --cwd --public-dir --jsx-runtime --platform --loader"; - [[ - ( -n "${replaced_script}" && "${replaced_script}" == "${prev}" ) || \ - ( "${global_option_with_extra_args}" =~ ${re_prev_prev} ) - ]] && return; - unset COMPREPLY; - } + [[ "${cur_word}" != -* ]] && { + _read_scripts_in_package_json; + COMPREPLY+=( $(compgen -W "${SUBCOMMANDS}" -- "${cur_word}") ); + _file_arguments "${RUN_EXTS}"; } return;; + *) + _long_short_completion \ + "${GLOBAL_OPTIONS[*]}" \ + "${GLOBAL_OPTIONS[SHORT_OPTIONS]}" + [[ "${cur_word}" != -* ]] && COMPREPLY+=( $(compgen -f -- "${cur_word}") ); + return;; esac } diff --git a/completions/bun.fish b/completions/bun.fish index 5e94166f5ecb..b6d6f1cc1bee 100644 --- a/completions/bun.fish +++ b/completions/bun.fish @@ -37,12 +37,43 @@ set -l bun_install_boolean_flags_descriptions "Write a yarn.lock file (yarn v1)" set -l bun_builtin_cmds_without_run dev create help bun upgrade discord install remove add update audit dedupe prune init pm x repl set -l bun_builtin_cmds_accepting_flags create help bun upgrade discord run init link unlink pm x update +# Subcommands (and their aliases, as accepted by `bun `) whose arguments +# are never file paths. Anything else in the first position (`run`, `test`, +# `build`, or a script/file being executed) takes files. +set -l bun_cmds_without_file_args $bun_builtin_cmds_without_run i ci a r rm uninstall c up link unlink outdated publish patch info exec why whoami list completions + +function __bun_first_positional -d "Print the first non-flag word after bun, skipping values of known arg-taking runtime flags" + # Same token walk as fish's own __fish_use_subcommand; `set -e` rather than + # an open-ended `[2..]` range, which needs fish 3.4. + set -l tokens (commandline -poc) + set -e tokens[1] + set -l skip 0 + for tok in $tokens + if test $skip -eq 1 + set skip 0 + continue + end + switch $tok + case --cwd --preload --require --import --env-file --config --define --loader --port --origin --use --filter --eval --print --title --shell --install --conditions --unhandled-rejections --dns-result-order --console-depth --elide-lines --fetch-preconnect --max-http-header-size --tsconfig-override -r -e -p -d -l -u -c -F + set skip 1 + case '-*' + case '*' + echo $tok + return + end + end +end + +function __bun_use_subcommand -d "Like __fish_use_subcommand but skips values of known arg-taking runtime flags" + set -l first (__bun_first_positional) + test -z "$first" +end function __bun_complete_bins_scripts --inherit-variable bun_builtin_cmds_without_run -d "Emit bun completions for bins and scripts" # Do nothing if we already have a builtin subcommand, # or any subcommand other than "run". if __fish_seen_subcommand_from $bun_builtin_cmds_without_run - or not __fish_use_subcommand && not __fish_seen_subcommand_from run + or not __bun_use_subcommand && not __fish_seen_subcommand_from run return end # Do we already have a bin or script subcommand? @@ -60,26 +91,32 @@ function __bun_complete_bins_scripts --inherit-variable bun_builtin_cmds_without for script in $scripts echo $script end - # Emit binaries and JS files (but only if we're doing `bun run`). - if __fish_seen_subcommand_from run - for bin in $bins - echo "$bin"\t"package bin" - end - for file in (__fish__get_bun_bun_js_files) - echo "$file"\t"Bun.js" - end + # Emit binaries and JS files. + for bin in $bins + echo "$bin"\t"package bin" + end + for file in (__fish__get_bun_bun_js_files) + echo "$file"\t"Bun.js" end end +function __bun_takes_files --inherit-variable bun_cmds_without_file_args -d "True unless the first positional is a subcommand whose arguments are not files" + set -l first (__bun_first_positional) + not contains -- "$first" $bun_cmds_without_file_args +end + # Clear existing completions complete -e -c bun # Dynamically emit scripts and binaries complete -c bun -f -a "(__bun_complete_bins_scripts)" +# Complete file paths for `bun `, `bun run `, and the arguments that +# follow a script (`bun script.ts `), including through runtime flags. +complete -c bun -n __bun_takes_files -F # Complete flags if we have no subcommand or a flag-friendly one. -set -l flag_applies "__fish_use_subcommand; or __fish_seen_subcommand_from $bun_builtin_cmds_accepting_flags" +set -l flag_applies "__bun_use_subcommand; or __fish_seen_subcommand_from $bun_builtin_cmds_accepting_flags" complete -c bun \ -n $flag_applies --no-files -s 'u' -l 'origin' -r -d 'Server URL. Rewrites import paths' complete -c bun \ @@ -91,13 +128,35 @@ complete -c bun \ complete -c bun \ -n $flag_applies --no-files -l 'use' -r -d 'Use a framework (ex: next)' complete -c bun \ - -n $flag_applies --no-files -l 'hot' -r -d 'Enable hot reloading in Bun\'s JavaScript runtime' + -n $flag_applies --no-files -l 'hot' -d 'Enable hot reloading in Bun\'s JavaScript runtime' +complete -c bun \ + -n $flag_applies --no-files -l 'watch' -d 'Automatically restart the process on file change' +complete -c bun \ + -n $flag_applies --no-files -l 'no-clear-screen' -d 'Disable clearing the terminal screen on reload when --hot or --watch is enabled' +complete -c bun \ + -n $flag_applies --no-files -l 'smol' -d 'Use less memory, but run garbage collection more often' +complete -c bun \ + -n $flag_applies --no-files -s 'b' -l 'bun' -d 'Force a script or package to use Bun\'s runtime instead of Node.js' +complete -c bun \ + -n $flag_applies --no-files -l 'inspect' -d 'Activate Bun\'s debugger' +complete -c bun \ + -n $flag_applies --no-files -l 'inspect-brk' -d 'Activate Bun\'s debugger, set breakpoint on first line of code and wait' +complete -c bun \ + -n $flag_applies --no-files -l 'inspect-wait' -d 'Activate Bun\'s debugger, wait for a connection before executing' +complete -c bun \ + -n $flag_applies --no-files -l 'silent' -d 'Don\'t print the script command' +complete -c bun \ + -n $flag_applies -s 'r' -l 'preload' -r -d 'Import a module before other modules are loaded' +complete -c bun \ + -n $flag_applies -l 'cwd' -r -d 'Absolute path to resolve files & entry points from' +complete -c bun \ + -n $flag_applies -l 'env-file' -r -d 'Load environment variables from the specified file(s)' # Complete dev and create as first subcommand. complete -c bun \ - -n "__fish_use_subcommand" -a 'dev' -d 'Start dev server' + -n __bun_use_subcommand -a 'dev' -d 'Start dev server' complete -c bun \ - -n "__fish_use_subcommand" -a 'create' -f -d 'Create a new project from a template' + -n __bun_use_subcommand -a 'create' -f -d 'Create a new project from a template' # Complete "next" and "react" if we've seen "create". complete -c bun \ @@ -108,22 +167,22 @@ complete -c bun \ # Complete "upgrade" as first subcommand. complete -c bun \ - -n "__fish_use_subcommand" -a 'upgrade' -d 'Upgrade bun to the latest version' -x + -n __bun_use_subcommand -a 'upgrade' -d 'Upgrade bun to the latest version' -x # Complete "-h/--help" unconditionally. complete -c bun \ -s "h" -l "help" -d 'See all commands and flags' -x # Complete "-v/--version" if we have no subcommand. complete -c bun \ - -n "not __fish_use_subcommand" -l "version" -s "v" -d 'Bun\'s version' -x + -n "not __bun_use_subcommand" -l "version" -s "v" -d 'Bun\'s version' -x # Complete additional subcommands. complete -c bun \ - -n "__fish_use_subcommand" -a 'discord' -d 'Open bun\'s Discord server' -x + -n __bun_use_subcommand -a 'discord' -d 'Open bun\'s Discord server' -x complete -c bun \ - -n "__fish_use_subcommand" -a 'bun' -d 'Generate a new bundle' + -n __bun_use_subcommand -a 'bun' -d 'Generate a new bundle' complete -c bun \ @@ -134,16 +193,16 @@ complete -c bun \ complete -c bun \ - -n "__fish_use_subcommand" -a 'init' -F -d 'Start an empty Bun project' + -n __bun_use_subcommand -a 'init' -F -d 'Start an empty Bun project' complete -c bun \ - -n "__fish_use_subcommand" -a 'install' -f -d 'Install packages from package.json' + -n __bun_use_subcommand -a 'install' -f -d 'Install packages from package.json' complete -c bun \ - -n "__fish_use_subcommand" -a 'add' -F -d 'Add a package to package.json' + -n __bun_use_subcommand -a 'add' -F -d 'Add a package to package.json' complete -c bun \ - -n "__fish_use_subcommand" -a 'remove' -F -d 'Remove a package from package.json' + -n __bun_use_subcommand -a 'remove' -F -d 'Remove a package from package.json' for i in (seq (count $bun_install_boolean_flags)) @@ -197,22 +256,22 @@ complete -c bun \ -n "__fish_seen_subcommand_from pm; and __fish_seen_subcommand_from licenses" -l 'filter' -s 'F' -d 'List only the matching workspaces' -r # Add built-in subcommands with descriptions. -complete -c bun -n "__fish_use_subcommand" -a "create" -f -d "Create a new project from a template" -complete -c bun -n "__fish_use_subcommand" -a "build bun" --require-parameter -F -d "Transpile and bundle one or more files" -complete -c bun -n "__fish_use_subcommand" -a "upgrade" -d "Upgrade Bun" -complete -c bun -n "__fish_use_subcommand" -a "run" -d "Run a script or package binary" -complete -c bun -n "__fish_use_subcommand" -a "install" -d "Install dependencies from package.json" -f -complete -c bun -n "__fish_use_subcommand" -a "remove" -d "Remove a dependency from package.json" -f -complete -c bun -n "__fish_use_subcommand" -a "add" -d "Add a dependency to package.json" -f -complete -c bun -n "__fish_use_subcommand" -a "init" -d "Initialize a Bun project in this directory" -f -complete -c bun -n "__fish_use_subcommand" -a "link" -d "Register or link a local npm package" -f -complete -c bun -n "__fish_use_subcommand" -a "unlink" -d "Unregister a local npm package" -f -complete -c bun -n "__fish_use_subcommand" -a "pm" -d "Additional package management utilities" -f -complete -c bun -n "__fish_use_subcommand" -a "x" -d "Execute a package binary, installing if needed" -f -complete -c bun -n "__fish_use_subcommand" -a "outdated" -d "Display the latest versions of outdated dependencies" -f -complete -c bun -n "__fish_use_subcommand" -a "audit" -d "Check installed packages for vulnerabilities" -f -complete -c bun -n "__fish_use_subcommand" -a "dedupe" -d "Remove duplicate versions from the lockfile" -f -complete -c bun -n "__fish_use_subcommand" -a "prune" -d "Remove packages that are not in the lockfile from node_modules" -f +complete -c bun -n __bun_use_subcommand -a "create" -f -d "Create a new project from a template" +complete -c bun -n __bun_use_subcommand -a "build bun" --require-parameter -F -d "Transpile and bundle one or more files" +complete -c bun -n __bun_use_subcommand -a "upgrade" -d "Upgrade Bun" +complete -c bun -n __bun_use_subcommand -a "run" -d "Run a script or package binary" +complete -c bun -n __bun_use_subcommand -a "install" -d "Install dependencies from package.json" -f +complete -c bun -n __bun_use_subcommand -a "remove" -d "Remove a dependency from package.json" -f +complete -c bun -n __bun_use_subcommand -a "add" -d "Add a dependency to package.json" -f +complete -c bun -n __bun_use_subcommand -a "init" -d "Initialize a Bun project in this directory" -f +complete -c bun -n __bun_use_subcommand -a "link" -d "Register or link a local npm package" -f +complete -c bun -n __bun_use_subcommand -a "unlink" -d "Unregister a local npm package" -f +complete -c bun -n __bun_use_subcommand -a "pm" -d "Additional package management utilities" -f +complete -c bun -n __bun_use_subcommand -a "x" -d "Execute a package binary, installing if needed" -f +complete -c bun -n __bun_use_subcommand -a "outdated" -d "Display the latest versions of outdated dependencies" -f +complete -c bun -n __bun_use_subcommand -a "audit" -d "Check installed packages for vulnerabilities" -f +complete -c bun -n __bun_use_subcommand -a "dedupe" -d "Remove duplicate versions from the lockfile" -f +complete -c bun -n __bun_use_subcommand -a "prune" -d "Remove packages that are not in the lockfile from node_modules" -f complete -c bun -n "__fish_seen_subcommand_from audit; and not __fish_seen_subcommand_from fix" -a "fix" -d "Upgrade vulnerable packages to the lowest safe version" -f complete -c bun -n "__fish_seen_subcommand_from audit" -l "json" -d "Output in JSON format" -f complete -c bun -n "__fish_seen_subcommand_from audit" -l "audit-level" -r -a "low moderate high critical" -d "Only print advisories at or above this severity" -f @@ -229,7 +288,7 @@ complete -c bun -n "__fish_seen_subcommand_from prune" -l "linker" -r -a "isolat complete -c bun -n "__fish_seen_subcommand_from prune" -s "F" -l "filter" -r -d "Prune only the matching workspaces" -f complete -c bun -n "__fish_seen_subcommand_from prune" -l "silent" -d "Don't log anything" -f complete -c bun -n "__fish_seen_subcommand_from audit prune" -l "cwd" -r -d "Set a specific cwd" -complete -c bun -n "__fish_use_subcommand" -a "update" -d "Update dependencies to their latest versions" -f +complete -c bun -n __bun_use_subcommand -a "update" -d "Update dependencies to their latest versions" -f complete -c bun -n "__fish_seen_subcommand_from update" -s "p" -l "production" -d "Only update dependencies and optionalDependencies" -f complete -c bun -n "__fish_seen_subcommand_from update" -s "P" -l "prod" -d "Only update dependencies and optionalDependencies" -f complete -c bun -n "__fish_seen_subcommand_from update" -s "d" -l "dev" -d "Only update devDependencies" -f @@ -248,8 +307,8 @@ complete -c bun -n "__fish_seen_subcommand_from update" -s "f" -l "force" -d "Al complete -c bun -n "__fish_seen_subcommand_from update" -l "no-cache" -d "Ignore manifest cache entirely" -f complete -c bun -n "__fish_seen_subcommand_from update" -l "silent" -d "Don't log anything" -f complete -c bun -n "__fish_seen_subcommand_from update" -l "verbose" -d "Excessively verbose logging" -f -complete -c bun -n "__fish_use_subcommand" -a "publish" -d "Publish your package from local to npm" -f -complete -c bun -n "__fish_use_subcommand" -a "repl" -d "Start a REPL session with Bun" -f +complete -c bun -n __bun_use_subcommand -a "publish" -d "Publish your package from local to npm" -f +complete -c bun -n __bun_use_subcommand -a "repl" -d "Start a REPL session with Bun" -f complete -c bun -n "__fish_seen_subcommand_from repl" -s "e" -l "eval" -r -d "Evaluate argument as a script, then exit" -f complete -c bun -n "__fish_seen_subcommand_from repl" -s "p" -l "print" -r -d "Evaluate argument as a script, print the result, then exit" -f complete -c bun -n "__fish_seen_subcommand_from repl" -s "r" -l "preload" -r -d "Import a module before other modules are loaded" diff --git a/completions/bun.zsh b/completions/bun.zsh index 3a02d58b200c..808fc24defe6 100644 --- a/completions/bun.zsh +++ b/completions/bun.zsh @@ -858,9 +858,59 @@ _bun() { local curcontext="$curcontext" state line context # ---- Command: - _arguments -s \ + # Runtime flags are declared here so that `_arguments` skips over them when + # locating the first positional word; otherwise `bun --hot run …` dispatches + # on `--hot` and completes nothing. `-A '-*'` stops option matching at the + # first positional so that e.g. `-d` after `bun add` is left for + # `_bun_add_completion` instead of being claimed here as `--define`. + _arguments -s -A '-*' \ '1: :->cmd' \ - '*: :->args' && + '*:: :->args' \ + '--hot[Enable auto reload in Bun'"'"'s JavaScript runtime]' \ + '--watch[Automatically restart the process on file change]' \ + '--no-clear-screen[Disable clearing the terminal screen on reload when --hot or --watch is enabled]' \ + '--smol[Use less memory, but run garbage collection more often]' \ + '--bun[Force a script or package to use Bun'"'"'s runtime instead of Node.js (via symlinking node)]' \ + '-b[Force a script or package to use Bun'"'"'s runtime instead of Node.js (via symlinking node)]' \ + '-i[Automatically install dependencies and use global cache in Bun'"'"'s runtime, equivalent to --install=fallback]' \ + '--silent[Don'"'"'t print the script command]' \ + '--inspect=-[Activate Bun'"'"'s debugger]::addr' \ + '--inspect-wait=-[Activate Bun'"'"'s debugger, wait for a connection before executing]::addr' \ + '--inspect-brk=-[Activate Bun'"'"'s debugger, set breakpoint on first line of code and wait]::addr' \ + '*--preload[Import a module before other modules are loaded]:preload:_files' \ + '*-r[Import a module before other modules are loaded]:preload:_files' \ + '*--require[Import a module before other modules are loaded]:preload:_files' \ + '*--import[Import a module before other modules are loaded]:preload:_files' \ + '--cwd[Absolute path to resolve files & entry points from]:cwd:_files -/' \ + '--config=-[Specify path to Bun config file]::config:_files' \ + '*--env-file[Load environment variables from the specified file(s)]:env-file:_files' \ + '--port[Set the default port for Bun.serve]:port' \ + '-p[Evaluate argument as a script and print the result]:script' \ + '--print[Evaluate argument as a script and print the result]:script' \ + '-e[Evaluate argument as a script]:script' \ + '--eval[Evaluate argument as a script]:script' \ + '*--filter[Run a script in all workspace packages matching the pattern]:filter' \ + '*-F[Run a script in all workspace packages matching the pattern]:filter' \ + '--title[Set the process title]:title' \ + '--shell[Control the shell used for package.json scripts]:shell:(bun system)' \ + '--install[Configure auto-install behavior]:install:(auto fallback force)' \ + '*--conditions[Pass custom conditions to resolve]:conditions' \ + '--unhandled-rejections[Set unhandled rejection behavior]:mode:(strict throw warn none warn-with-error-code)' \ + '--dns-result-order[Set the default order of DNS lookup results]:order:(verbatim ipv4first ipv6first)' \ + '--console-depth[Set the default depth for console.log object inspection]:depth' \ + '--elide-lines[Number of lines of script output shown when using --filter]:lines' \ + '*--fetch-preconnect[Preconnect to a URL while code is loading]:url' \ + '--max-http-header-size[Set the maximum size of HTTP headers in bytes]:bytes' \ + '*--define[Substitute K:V while parsing]:define' \ + '*-d[Substitute K:V while parsing]:define' \ + '*--loader[Parse files with .ext:loader]:loader' \ + '*-l[Parse files with .ext:loader]:loader' \ + '--tsconfig-override[Load tsconfig from path instead of cwd/tsconfig.json]:tsconfig:_files' \ + '--version[Print version and exit]' \ + '-v[Print version and exit]' \ + '--revision[Print version with revision and exit]' \ + '--help[Display this menu and exit]' \ + '-h[Display this menu and exit]' && ret=0 case $state in @@ -894,10 +944,17 @@ _bun() { 'help\:"Show all supported flags and commands" ' ) main_commands=($main_commands) - _alternative "$scripts" "args:command:(($main_commands))" "files:files:compadd -a files_list" + _alternative "$scripts" "args:command:(($main_commands))" "files:files:compadd -a files_list" "globbed-files:file:_files -g '*.(js|mjs|cjs|ts|mts|cts|jsx|tsx|wasm|html)'" ;; args) + # `*:: :->args` above rewrote `words`/`CURRENT` to contain only the + # positional arguments (subcommand onward). Prepend the program name so + # each sub-completer's `_arguments '1: :->cmd' …` sees the layout it was + # written for, independent of whatever runtime flags preceded the + # subcommand. + words=($program "${words[@]}") + (( CURRENT++ )) case $line[1] in add|a) _bun_add_completion @@ -1086,6 +1143,10 @@ _bun() { ;; esac + ;; + *) + _files + ;; esac @@ -1105,7 +1166,7 @@ _bun_run_param_script_completion() { _alternative "scripts:scripts:compadd -a scripts_list" _alternative "bin:bin:compadd -a bins" - _alternative "files:file:_files -g '*.(js|ts|jsx|tsx|wasm)'" + _alternative "files:file:_files -g '*.(js|mjs|cjs|ts|mts|cts|jsx|tsx|wasm|html)'" } _bun_link_param_package_completion() { diff --git a/test/cli/completions.test.ts b/test/cli/completions.test.ts new file mode 100644 index 000000000000..30cfaaeb4907 --- /dev/null +++ b/test/cli/completions.test.ts @@ -0,0 +1,391 @@ +// Regression coverage for https://github.com/oven-sh/bun/issues/7805: +// `bun ` (no `run`) offered no file completions, and any runtime flag +// before the subcommand (`bun --hot run …`, `bun --watch …`) made every shell +// dispatch on the flag instead of the first positional word and complete +// nothing. +// +// `bun completions` writes the embedded completion script for the shell named +// by $SHELL to stdout when stdout is not a TTY, so the assertions run against +// the bytes that ship inside the binary. + +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; +import { chmodSync } from "node:fs"; +import { join } from "node:path"; + +async function embeddedCompletions(shell: "fish" | "bash" | "zsh"): Promise { + using home = tempDir(`bun-completions-${shell}`, {}); + await using proc = Bun.spawn({ + cmd: [bunExe(), "completions"], + env: { ...bunEnv, SHELL: `/bin/${shell}`, HOME: String(home), BUN_INSTALL: String(home) }, + cwd: String(home), + 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; +} + +// The bash script uses `declare -A` (bash >= 4.0). macOS ships bash 3.2 as +// /bin/bash, so the functional probe needs a modern bash on PATH. +const bashMajor = (() => { + if (isWindows || !Bun.which("bash")) return 0; + try { + const { stdout } = Bun.spawnSync({ cmd: ["bash", "-c", 'printf "%s" "${BASH_VERSINFO[0]}"'], env: bunEnv }); + return parseInt(stdout.toString().trim(), 10) || 0; + } catch { + return 0; + } +})(); + +const fishBin = Bun.which("fish"); +const zshBin = Bun.which("zsh"); + +const fixtureFiles = { + "package.json": JSON.stringify({ name: "fixture", scripts: { myscript: "echo hi" } }), + "entry.ts": 'console.log("entry");', + "notes.txt": "not runnable", + "src/index.ts": 'console.log("index");', + "src/other.ts": 'console.log("other");', +}; + +describe.skipIf(isWindows)("shell completions: `bun ` and runtime flags (#7805)", () => { + async function bashComplete(dir: string, line: string): Promise { + // bash-completion (loaded in interactive shells) turns extglob on, so the + // probe does too; `_read_scripts_in_package_json` only parses package.json + // scripts when extglob is enabled. + const probe = + "shopt -s extglob\n" + + "source ./bun.bash\n" + + "words=(" + + line + .trimEnd() + .split(" ") + .map(w => JSON.stringify(w)) + .join(" ") + + ")\n" + + (line.endsWith(" ") ? 'words+=("")\n' : "") + + 'COMP_WORDS=("${words[@]}")\n' + + "COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))\n" + + `COMP_LINE=${JSON.stringify(line)}\n` + + "COMP_POINT=${#COMP_LINE}\n" + + "COMPREPLY=()\n" + + "_bun_completions\n" + + 'for w in "${COMPREPLY[@]}"; do printf \'%s\\n\' "$w"; done\n'; + await using proc = Bun.spawn({ + cmd: ["bash", "-c", probe], + cwd: dir, + env: { ...bunEnv, PATH: process.env.PATH }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + return stdout.split("\n").filter(Boolean); + } + + test.concurrent.skipIf(bashMajor < 4)( + "bash: completes files for `bun ` with and without runtime flags", + async () => { + const script = await embeddedCompletions("bash"); + using dir = tempDir("bun-bash-completion-7805", { "bun.bash": script, ...fixtureFiles }); + + for (const line of [ + "bun run src/", + "bun src/", + "bun --hot run src/", + "bun run --watch src/", + "bun --hot src/", + "bun --watch src/", + "bun --smol --hot src/", + "bun --inspect src/", + ]) { + const reply = await bashComplete(String(dir), line); + expect(reply, `line: ${JSON.stringify(line)}`).toContain("src/index.ts"); + expect(reply, `line: ${JSON.stringify(line)}`).toContain("src/other.ts"); + } + + // After a flag the subcommand list and `run` should still be offered. + const afterFlag = await bashComplete(String(dir), "bun --hot "); + expect(afterFlag).toContain("run"); + expect(afterFlag).toContain("install"); + expect(afterFlag).toContain("--watch"); + + // With nothing typed yet, runnable files and directories are listed and + // the extension filter still applies. (The old empty-word branch passed + // `-fG`, which made compgen read `-X` as the glob and list nothing.) + for (const line of ["bun ", "bun run ", "bun --hot "]) { + const reply = await bashComplete(String(dir), line); + expect(reply, `line: ${JSON.stringify(line)}`).toContain("entry.ts"); + expect(reply, `line: ${JSON.stringify(line)}`).toContain("src"); + expect(reply, `line: ${JSON.stringify(line)}`).not.toContain("notes.txt"); + } + }, + ); + + test.concurrent.skipIf(bashMajor < 4)("bash: flag-taking options don't swallow the subcommand", async () => { + const script = await embeddedCompletions("bash"); + using dir = tempDir("bun-bash-completion-7805b", { "bun.bash": script, ...fixtureFiles }); + + const reply = await bashComplete(String(dir), "bun --cwd " + String(dir) + " run src/"); + expect(reply).toContain("src/index.ts"); + }); + + test.concurrent.skipIf(bashMajor < 4)( + "bash: completes files for the arguments after `bun