Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for completion in Bash #403

Merged
merged 20 commits into from
Jul 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
179 changes: 177 additions & 2 deletions completions/bun.bash
Original file line number Diff line number Diff line change
@@ -1,8 +1,183 @@
#/usr/bin/env bash

# This is not implemented yet.
# But a PR implementing it would be very welcome!

_file_arguments() {
shopt -s extglob globstar
local extensions="${1}";

if [[ -z "${cur_word}" ]]; then
COMPREPLY=( $(compgen -fG -X "${extensions}" -- "${cur_word}") );
else
COMPREPLY=( $(compgen -f -X "${extensions}" -- "${cur_word}") );
fi
shopt -u extglob globstar
}

_long_short_completion() {
local wordlist="${1}";
local short_options="${2}"

[[ -z "${cur_word}" || "${cur_word}" =~ ^- ]] && {
COMPREPLY=( $(compgen -W "${wordlist}" -- "${cur_word}"));
return;
}
[[ "${cur_word}" =~ ^-[A-Za_z]+ ]] && {
COMPREPLY=( $(compgen -W "${short_options}" -- "${cur_word}"));
return;
}
}

# loads the scripts block in package.json
_read_scripts_in_package_json() {
local package_json;
local return_package_json
local line=0;
local working_dir="${PWD}";

for ((; line < ${#COMP_WORDS[@]}; line+=1)); do
[[ "${COMP_WORDS[${line}]}" == "--cwd" ]] && working_dir="${COMP_WORDS[$((line + 1))]}";
done

[[ -f "${working_dir}/package.json" ]] && package_json=$(<${working_dir}/package.json);

[[ "${package_json}" =~ "\"scripts\""[[:space:]]*":"[[:space:]]*\{(.*)\} ]] && {
local package_json_compreply;
local matched="${BASH_REMATCH[@]:1}";
local scripts="${matched%%\}*}";
shopt -s extglob;
scripts="${scripts//@(\"|\')/}";
shopt -u extglob;
readarray -td, scripts <<<"${scripts}";
for completion in "${scripts[@]}"; do
package_json_compreply+=( "${completion%:*}" );
done
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]}" ) || \
( "${COMPREPLY[*]}" =~ ${re_comp_word_script} )
]] && {
local re_script=$(echo ${package_json_compreply[@]} | sed 's/[^ ]*/(&)/g');
local new_reply=$(echo "${COMPREPLY[@]}" | sed -E "s/$re_script//");
COMPREPLY=( $(compgen -W "${new_reply}" -- "${cur_word}") );
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}") );
}
}


_bun_completions() {
declare -A GLOBAL_OPTIONS;
declare -A PACKAGE_OPTIONS;

local SUBCOMMANDS="dev bun create run install add remove upgrade completions discord help";

GLOBAL_OPTIONS[LONG_OPTIONS]="--use --cwd --bunfile --server-bunfile --config --disable-react-fast-refresh --disable-hmr --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";

PACKAGE_OPTIONS[ADD_OPTIONS_LONG]="--development --optional";
PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]="-d";
PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]="";
PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]="";

PACKAGE_OPTIONS[SHARED_OPTIONS_LONG]="--config --yarn --production --no-save --dry-run --lockfile --force --cache-dir --no-cache --silent --verbose --global --cwd --backend --link-native-bins --help";
PACKAGE_OPTIONS[SHARED_OPTIONS_SHORT]="-c -y -p -f -g";

local cur_word="${COMP_WORDS[${COMP_CWORD}]}";
local prev="${COMP_WORDS[$(( COMP_CWORD - 1 ))]}";

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
a|add|remove|rm|install|i)
COMPREPLY=( $(compgen -W "clonefile copyfile hardlink clonefile_each_dir" -- "${cur_word}") );
;;
esac
return ;;
--cwd|--public-dir)
COMPREPLY=( $(compgen -d -- "${cur_word}" ));
return;;
--jsx-runtime)
COMPREPLY=( $(compgen -W "automatic classic" -- "${cur_word}") );
return;;
--platform)
COMPREPLY=( $(compgen -W "browser node" -- "${cur_word}") );
return;;
-l|--loader)
[[ "${cur_word}" =~ (:) ]] && {
local cut_colon_forward="${cur_word%%:*}"
COMPREPLY=( $(compgen -W "${cut_colon_forward}:jsx ${cut_colon_forward}:js ${cut_colon_forward}:json ${cut_colon_forward}:tsx ${cut_colon_forward}:ts ${cut_colon_forward}:css" -- "${cut_colon_forward}:${cur_word##*:}") );
}
return;;
esac

case "${COMP_WORDS[1]}" in
help|completions|--help|-h|-v|--version) 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]}" \
"${PACKAGE_OPTIONS[ADD_OPTIONS_SHORT]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_SHORT]}"
return;;
remove|rm|i|install)
_long_short_completion \
"${PACKAGE_OPTIONS[REMOVE_OPTIONS_LONG]} ${PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_LONG]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_SHORT]}" \
"${PACKAGE_OPTIONS[REMOVE_OPTIONS_SHORT]} ${PACKAGE_OPTIONS[SHARED_OPTIONS_SHORT]}";
return;;
create|c)
COMPREPLY=( $(compgen -W "--force --no-install --help --no-git --verbose --no-package-json --open next react" -- "${cur_word}") );
return;;
upgrade)
COMPREPLY=( $(compgen -W "--version --cwd --help -v -h") );
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;
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}" ]] && {
declare -A comp_reply_associative="( $(echo ${COMPREPLY[@]} | sed 's/[^ ]*/[&]=&/g') )";
[[ -z "${comp_reply_associative[${prev}]}" ]] && {
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;
}
}
return;;
esac

}

complete -F _bun_completions bun
58 changes: 52 additions & 6 deletions src/cli/install_completions_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ pub const InstallCompletionsCommand = struct {
const fail_exit_code: u8 = if (std.os.getenvZ("IS_BUN_AUTO_UPDATE") == null) 1 else 0;

switch (shell) {
.bash => {
Output.prettyErrorln("<r><red>error:<r> Bash completions aren't implemented yet, just zsh & fish. A PR is welcome!", .{});
Global.exit(fail_exit_code);
},
.unknown => {
Output.prettyErrorln("<r><red>error:<r> Unknown or unsupported shell. Please set $SHELL to one of zsh, fish, or bash. To manually output completions, run this:\n bun getcompletes", .{});
Global.exit(fail_exit_code);
Expand Down Expand Up @@ -224,7 +220,57 @@ pub const InstallCompletionsCommand = struct {
break :found std.fs.openDirAbsolute(dir, .{ .iterate = true }) catch continue;
}
},
.bash => {},
.bash => {
if (std.os.getenvZ("XDG_DATA_HOME")) |data_dir| {
outer: {
var paths = [_]string{ std.mem.span(data_dir), "./bash-completion/completions" };
completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto);
break :found std.fs.openDirAbsolute(completions_dir, .{ .iterate = true }) catch
break :outer;
}
}

if (std.os.getenvZ("XDG_CONFIG_HOME")) |config_dir| {
outer: {
var paths = [_]string{ std.mem.span(config_dir), "./bash-completion/completions" };
completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto);

break :found std.fs.openDirAbsolute(completions_dir, .{ .iterate = true }) catch
break :outer;
}
}

if (std.os.getenvZ("HOME")) |home_dir| {
{
outer: {
var paths = [_]string{ std.mem.span(home_dir), "./.oh-my-bash/custom/completions" };
completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto);

break :found std.fs.openDirAbsolute(completions_dir, .{ .iterate = true }) catch
break :outer;
}
}
{
outer: {
var paths = [_]string{ std.mem.span(home_dir), "./.bash_completion.d" };
completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto);

break :found std.fs.openDirAbsolute(completions_dir, .{ .iterate = true }) catch
break :outer;
}
}
}

const dirs_to_try = [_]string{
"/opt/homebrew/share/bash-completion/completions/",
"/opt/local/share/bash-completion/completions/",
};

for (dirs_to_try) |dir| {
completions_dir = dir;
break :found std.fs.openDirAbsolute(dir, .{ .iterate = true }) catch continue;
}
},
else => unreachable,
}

Expand All @@ -250,7 +296,7 @@ pub const InstallCompletionsCommand = struct {
const filename = switch (shell) {
.fish => "bun.fish",
.zsh => "_bun",
.bash => "_bun.bash",
.bash => "bun.completion.bash",
else => unreachable,
};

Expand Down