feat(completion): add bash and zsh completions

This commit is contained in:
Arvin
2026-04-24 22:23:47 +08:00
parent 24eeddc4ac
commit 631cf35354
5 changed files with 484 additions and 1 deletions
+1
View File
@@ -30,6 +30,7 @@ mark_install_plan || true
mark_install_port_plan || true
install_clashctl_entry
install_clashctl_completion
install_shell_alias_entry
install_runtime_entry
install_local_dashboard_assets
+5
View File
@@ -7,6 +7,7 @@ PROJECT_DIR="$(cd "$(dirname "$SCRIPT_PATH")/../.." && pwd)"
source "$PROJECT_DIR/scripts/core/common.sh"
source "$PROJECT_DIR/scripts/core/runtime.sh"
source "$PROJECT_DIR/scripts/core/config.sh"
source "$PROJECT_DIR/scripts/core/completion.sh"
source "$PROJECT_DIR/scripts/core/proxy.sh"
source "$PROJECT_DIR/scripts/core/update.sh"
source "$PROJECT_DIR/scripts/init/systemd.sh"
@@ -42,6 +43,7 @@ Usage:
status 🔍️ 查看状态总览
boot 🚦 管理开机代理接管
log/logs 📜 查看日志
completion 💡 导出 Bash / Zsh 补全脚本
💡 更多高级能力:clashctl help advanced
EOF
@@ -71,6 +73,7 @@ usage_advanced() {
boot proxy on|off|status 📜 仅管理开机代理保持
upgrade 🚀 升级当前或指定内核
update 🔄 更新项目代码
completion bash|zsh 💡 导出 Shell 补全脚本
dev reset 🧪 恢复到安装前状态(保留项目目录和已下载文件)
📌 Advanced Examples:
@@ -2976,6 +2979,7 @@ cmd_ui_help_summary() {
printf ' %-18s %s\n' "clashctl sub" "🧩 订阅高级管理(启用 / 禁用 / 重命名 / 删除)"
printf ' %-18s %s\n' "clashctl upgrade" "🚀 升级当前或指定内核"
printf ' %-18s %s\n' "clashctl update" "🔄 更新项目代码"
printf ' %-18s %s\n' "clashctl completion" "💡 导出 Bash / Zsh 补全脚本"
echo "📜 日志"
printf ' %-18s %s\n' "clashctl doctor" "🩺 诊断面板"
printf ' %-18s %s\n' "clashctl log/logs" "📜 查看日志"
@@ -7275,6 +7279,7 @@ case "$cmd" in
proxy) cmd_proxy "$@" ;;
upgrade) cmd_upgrade "$@" ;;
update) cmd_update "$@" ;;
completion) cmd_completion "$@" ;;
start-direct) cmd_start_direct "$@" ;;
stop-direct) cmd_stop_direct "$@" ;;
restart-direct) cmd_restart_direct "$@" ;;
+58 -1
View File
@@ -2307,6 +2307,30 @@ alias_source_file() {
echo "$PROJECT_DIR/scripts/core/alias.sh"
}
completion_dir() {
if [ "$INSTALL_SCOPE" = "system" ]; then
echo "/etc/profile.d"
else
echo "$HOME/.config/clash-for-linux"
fi
}
bash_completion_entry_file() {
if [ "$INSTALL_SCOPE" = "system" ]; then
echo "$(completion_dir)/clash-for-linux-completion.bash"
else
echo "$(completion_dir)/completion.bash"
fi
}
zsh_completion_entry_file() {
if [ "$INSTALL_SCOPE" = "system" ]; then
echo "$(completion_dir)/clash-for-linux-completion.zsh"
else
echo "$(completion_dir)/completion.zsh"
fi
}
clashctl_entry_target() {
echo "$(command_install_dir)/clashctl"
}
@@ -2411,12 +2435,14 @@ cleanup_legacy_shell_entries() {
}
install_shell_alias_entry() {
local profile_file alias_file shell_rc
local profile_file alias_file shell_rc bash_completion_file zsh_completion_file
cleanup_legacy_shell_entries
profile_file="$(profile_entry_file)"
alias_file="$(alias_source_file)"
bash_completion_file="$(bash_completion_entry_file)"
zsh_completion_file="$(zsh_completion_entry_file)"
mkdir -p "$(dirname "$profile_file")"
[ -f "$alias_file" ] || die "未找到 alias 脚本:$alias_file"
@@ -2430,6 +2456,18 @@ if [ -n "\${BASH_VERSION:-}" ] && [ -z "\${CLASH_FOR_LINUX_SHELL_LOADED:-}" ]; t
export CLASH_FOR_LINUX_SHELL_LOADED="1"
source "$alias_file"
fi
case "\$-" in
*i*)
if [ -n "\${BASH_VERSION:-}" ] && [ -z "\${CLASH_FOR_LINUX_BASH_COMPLETION_LOADED:-}" ] && [ -f "$bash_completion_file" ]; then
export CLASH_FOR_LINUX_BASH_COMPLETION_LOADED="1"
source "$bash_completion_file"
elif [ -n "\${ZSH_VERSION:-}" ] && [ -z "\${CLASH_FOR_LINUX_ZSH_COMPLETION_LOADED:-}" ] && [ -f "$zsh_completion_file" ]; then
export CLASH_FOR_LINUX_ZSH_COMPLETION_LOADED="1"
source "$zsh_completion_file"
fi
;;
esac
EOF
chmod +x "$profile_file"
@@ -2440,6 +2478,21 @@ EOF
install_alias_command_wrappers
}
install_clashctl_completion() {
local bash_target zsh_target
bash_target="$(bash_completion_entry_file)"
zsh_target="$(zsh_completion_entry_file)"
mkdir -p "$(dirname "$bash_target")"
mkdir -p "$(dirname "$zsh_target")"
bash "$PROJECT_DIR/scripts/core/clashctl.sh" completion bash > "$bash_target"
bash "$PROJECT_DIR/scripts/core/clashctl.sh" completion zsh > "$zsh_target"
chmod +x "$bash_target" "$zsh_target" 2>/dev/null || true
}
remove_clashctl_entry() {
rm -f "$(clashctl_entry_target)" "$(clashctl_bin_entry_target)" 2>/dev/null || true
remove_alias_command_wrappers
@@ -2461,6 +2514,10 @@ remove_shell_alias_entry() {
fi
}
remove_clashctl_completion() {
rm -f "$(bash_completion_entry_file)" "$(zsh_completion_entry_file)" 2>/dev/null || true
}
install_has_subscription() {
[ -n "$(subscription_url 2>/dev/null || true)" ]
}
+419
View File
@@ -0,0 +1,419 @@
#!/usr/bin/env bash
completion_emit_script_body() {
printf '_clash_for_linux_project_dir=%q\n' "$PROJECT_DIR"
cat <<'EOF'
_clash_for_linux_runtime_dir="${_clash_for_linux_project_dir}/runtime"
_clash_for_linux_subscription_file="${_clash_for_linux_runtime_dir}/subscriptions.yaml"
_clash_for_linux_mixin_file="${_clash_for_linux_runtime_dir}/mixin.yaml"
_clash_for_linux_local_subscription_dir="${_clash_for_linux_runtime_dir}/subscriptions"
_clash_for_linux_yq_bin="${_clash_for_linux_runtime_dir}/bin/yq"
# Hard constraints for completion:
# - local-only and offline
# - no controller or network access
# - low-latency best effort with immediate silent fallback
# - skip YAML-based dynamic completion when yq is missing or fails
_clash_for_linux_add_matches() {
local cur="$1"
local candidate
shift
for candidate in "$@"; do
[ -n "${candidate:-}" ] || continue
if [ -z "$cur" ] || [ "${candidate:0:${#cur}}" = "$cur" ]; then
COMPREPLY+=("$candidate")
fi
done
}
_clash_for_linux_add_stream_matches() {
local cur="$1"
local candidate
while IFS= read -r candidate; do
[ -n "${candidate:-}" ] || continue
[ "$candidate" = "null" ] && continue
if [ -z "$cur" ] || [ "${candidate:0:${#cur}}" = "$cur" ]; then
COMPREPLY+=("$candidate")
fi
done
}
_clash_for_linux_add_subscription_matches() {
local cur="$1"
[ -x "$_clash_for_linux_yq_bin" ] || return 0
[ -s "$_clash_for_linux_subscription_file" ] || return 0
_clash_for_linux_add_stream_matches "$cur" < <(
"$_clash_for_linux_yq_bin" eval '.sources | keys | .[]' "$_clash_for_linux_subscription_file" 2>/dev/null
)
}
_clash_for_linux_add_relay_matches() {
local cur="$1"
[ -x "$_clash_for_linux_yq_bin" ] || return 0
[ -s "$_clash_for_linux_mixin_file" ] || return 0
_clash_for_linux_add_stream_matches "$cur" < <(
"$_clash_for_linux_yq_bin" eval '(.append["proxy-groups"] // [])[] | select(.type == "relay") | .name' "$_clash_for_linux_mixin_file" 2>/dev/null
)
}
_clash_for_linux_add_local_subscription_matches() {
local cur="$1"
local path
[ -d "$_clash_for_linux_local_subscription_dir" ] || return 0
while IFS= read -r path; do
[ -n "${path:-}" ] || continue
if [ -z "$cur" ] || [ "${path:0:${#cur}}" = "$cur" ]; then
COMPREPLY+=("$path")
fi
done < <(
for path in "$_clash_for_linux_local_subscription_dir"/*; do
[ -f "$path" ] || continue
printf '%s\n' "${path##*/}"
done 2>/dev/null
)
}
_clash_for_linux_complete_add() {
local cur="$1"
local rel_index="$2"
local arg1="${3:-}"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" local
return 0
fi
if [ "$arg1" = "local" ]; then
_clash_for_linux_add_local_subscription_matches "$cur"
fi
}
_clash_for_linux_complete_use() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" --recommend -r --verbose -v
case "$cur" in
-*) return 0 ;;
esac
_clash_for_linux_add_subscription_matches "$cur"
}
_clash_for_linux_complete_health() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" --verbose -v
case "$cur" in
-*) return 0 ;;
esac
_clash_for_linux_add_subscription_matches "$cur"
}
_clash_for_linux_complete_status() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" --verbose -v
}
_clash_for_linux_complete_boot() {
local cur="$1"
local rel_index="$2"
local arg1="${3:-}"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" on off status runtime proxy help -h --help
return 0
fi
case "$arg1" in
runtime|proxy)
if [ "$rel_index" -eq 2 ]; then
_clash_for_linux_add_matches "$cur" on off status
fi
;;
esac
}
_clash_for_linux_complete_config() {
local cur="$1"
local rel_index="$2"
local arg1="${3:-}"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" show explain regen kernel
return 0
fi
if [ "$arg1" = "kernel" ] && [ "$rel_index" -eq 2 ]; then
_clash_for_linux_add_matches "$cur" mihomo clash
fi
}
_clash_for_linux_complete_mixin() {
local cur="$1"
local rel_index="$2"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" edit raw runtime help -e -c -r --edit --raw --runtime -h --help
fi
}
_clash_for_linux_complete_relay() {
local cur="$1"
local rel_index="$2"
local arg1="${3:-}"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" add list ls remove rm delete help -h --help
return 0
fi
case "$arg1" in
remove|rm|delete)
if [ "$rel_index" -eq 2 ]; then
_clash_for_linux_add_relay_matches "$cur"
fi
;;
add)
_clash_for_linux_add_matches "$cur" --domain --match
;;
esac
}
_clash_for_linux_complete_sub() {
local cur="$1"
local rel_index="$2"
local arg1="${3:-}"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" list use set enable disable rename remove rm del health help -h --help
return 0
fi
case "$arg1" in
use|enable|disable|remove|rm|del)
if [ "$rel_index" -eq 2 ]; then
_clash_for_linux_add_subscription_matches "$cur"
fi
;;
rename)
if [ "$rel_index" -eq 2 ]; then
_clash_for_linux_add_subscription_matches "$cur"
fi
;;
health)
_clash_for_linux_add_matches "$cur" --verbose -v
if [ "$rel_index" -eq 2 ]; then
case "$cur" in
-*) return 0 ;;
esac
_clash_for_linux_add_subscription_matches "$cur"
fi
;;
esac
}
_clash_for_linux_complete_tun() {
local cur="$1"
local rel_index="$2"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" status on off doctor
fi
}
_clash_for_linux_complete_upgrade() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" mihomo clash -v --verbose
}
_clash_for_linux_complete_update() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" --force --regenerate
}
_clash_for_linux_complete_dev() {
local cur="$1"
local rel_index="$2"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" reset
fi
}
_clash_for_linux_complete_completion() {
local cur="$1"
local rel_index="$2"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" bash zsh
fi
}
_clash_for_linux_complete_help() {
local cur="$1"
local rel_index="$2"
COMPREPLY=()
if [ "$rel_index" -eq 1 ]; then
_clash_for_linux_add_matches "$cur" advanced
fi
}
_clash_for_linux_complete_top_level() {
local cur="$1"
COMPREPLY=()
_clash_for_linux_add_matches "$cur" \
add use ls health select on off status status-next \
boot log logs doctor ui secret tun dev config mixin \
relay profile sub proxy upgrade update completion help \
-h --help
}
_clash_for_linux_complete_command() {
local root cur canonical rel_index
local arg1="" arg2="" arg3=""
root="${COMP_WORDS[0]##*/}"
cur="${COMP_WORDS[COMP_CWORD]:-}"
case "$root" in
clashctl)
if [ "$COMP_CWORD" -eq 1 ]; then
_clash_for_linux_complete_top_level "$cur"
return 0
fi
canonical="${COMP_WORDS[1]:-}"
rel_index=$((COMP_CWORD - 1))
arg1="${COMP_WORDS[2]:-}"
arg2="${COMP_WORDS[3]:-}"
arg3="${COMP_WORDS[4]:-}"
;;
clashrelay)
canonical="relay"
rel_index=$COMP_CWORD
arg1="${COMP_WORDS[1]:-}"
arg2="${COMP_WORDS[2]:-}"
arg3="${COMP_WORDS[3]:-}"
;;
clashmixin)
canonical="mixin"
rel_index=$COMP_CWORD
arg1="${COMP_WORDS[1]:-}"
;;
clashsecret)
canonical="secret"
rel_index=$COMP_CWORD
arg1="${COMP_WORDS[1]:-}"
;;
clashupgrade)
canonical="upgrade"
rel_index=$COMP_CWORD
arg1="${COMP_WORDS[1]:-}"
;;
clashtun)
canonical="tun"
rel_index=$COMP_CWORD
arg1="${COMP_WORDS[1]:-}"
;;
*)
COMPREPLY=()
return 0
;;
esac
case "$canonical" in
add) _clash_for_linux_complete_add "$cur" "$rel_index" "$arg1" ;;
use) _clash_for_linux_complete_use "$cur" ;;
health) _clash_for_linux_complete_health "$cur" ;;
status) _clash_for_linux_complete_status "$cur" ;;
boot) _clash_for_linux_complete_boot "$cur" "$rel_index" "$arg1" ;;
config) _clash_for_linux_complete_config "$cur" "$rel_index" "$arg1" ;;
mixin) _clash_for_linux_complete_mixin "$cur" "$rel_index" ;;
relay) _clash_for_linux_complete_relay "$cur" "$rel_index" "$arg1" ;;
sub) _clash_for_linux_complete_sub "$cur" "$rel_index" "$arg1" ;;
tun) _clash_for_linux_complete_tun "$cur" "$rel_index" ;;
upgrade) _clash_for_linux_complete_upgrade "$cur" ;;
update) _clash_for_linux_complete_update "$cur" ;;
dev) _clash_for_linux_complete_dev "$cur" "$rel_index" ;;
completion) _clash_for_linux_complete_completion "$cur" "$rel_index" ;;
help) _clash_for_linux_complete_help "$cur" "$rel_index" ;;
*)
COMPREPLY=()
;;
esac
}
complete -F _clash_for_linux_complete_command clashctl
complete -F _clash_for_linux_complete_command clashrelay
complete -F _clash_for_linux_complete_command clashmixin
complete -F _clash_for_linux_complete_command clashsecret
complete -F _clash_for_linux_complete_command clashupgrade
complete -F _clash_for_linux_complete_command clashtun
EOF
}
completion_emit_bash_script() {
completion_emit_script_body
}
completion_emit_zsh_script() {
cat <<'EOF'
autoload -Uz bashcompinit 2>/dev/null || return 0
bashcompinit >/dev/null 2>&1 || return 0
EOF
completion_emit_script_body
}
cmd_completion() {
case "${1:-}" in
bash)
completion_emit_bash_script
;;
zsh)
completion_emit_zsh_script
;;
*)
die_usage "completion 参数不合法" "clashctl completion bash|zsh"
;;
esac
}
+1
View File
@@ -41,6 +41,7 @@ detect_install_scope auto
service_stop || true
remove_runtime_entry || true
remove_clashctl_entry || true
remove_clashctl_completion || true
remove_shell_alias_entry || true
clear_shell_proxy_persist_state || true