Make clash the primary CLI command

This commit is contained in:
Arvin
2026-07-30 17:25:15 +08:00
parent ef59c36521
commit 3be0915a71
12 changed files with 687 additions and 480 deletions
+88 -86
View File
@@ -20,7 +20,7 @@
- 🚀 **自动识别系统架构**:自动下载并使用对应 Clash 内核
- 🧪 **端口自动检测与分配**:避免冲突
- 🔄 **多订阅管理**:可以保存多个订阅,通过 `clashctl use` 切换当前主订阅。
- 🔄 **多订阅管理**:可以保存多个订阅,通过 `clash use` 切换当前主订阅。
- 💫 **节点选择**:使用编号交互选择策略组和节点。
- 🌐 **Tun 模式**:用于透明代理接管场景(需root方式安装)。
- 🧠 **Mixin 机制**:可按需追加/覆盖 Clash 配置
@@ -56,55 +56,57 @@ bash install.sh
```bash
〽️ 常用命令
clashon 🚀 开启代理
clashoff ⛔ 关闭代理
clashctl select 💫 选择节点
clashctl mode 🧭 查看或切换路由模式
clashtest 🌐 测试当前节点访问 Google / YouTube
clashon 🚀 开启代理
clashoff ⛔ 关闭代理
clash select 💫 选择节点
clash mode 🧭 查看或切换路由模式
clashtest 🌐 测试当前节点访问 Google / YouTube
🕹️ 控制台
clashui 🕹️ 查看 Web 控制台
clashui 🕹️ 查看 Web 控制台
📦 订阅
clashctl add ➕ 添加订阅
clashctl add local ➕ 从 runtime/subscriptions 导入本地订阅
clashctl use 💱 切换订阅
clashctl ls 📜 查看订阅列表
clash add ➕ 添加订阅
clash add local ➕ 从 runtime/subscriptions 导入本地订阅
clash use 💱 切换订阅
clash ls 📜 查看订阅列表
🔐 密钥管理
clashctl secret 🔑 查看密钥
clashctl secret 123 🔐 设置密钥
clash secret 🔑 查看密钥
clash secret 123 🔐 设置密钥
📌 高级
clashctl lan 🏠 局域网代理管理
clashctl tun 🧪 Tun 模式管理(需root方式安装)
clashctl boot 🚦 开机代理接管管理
clashctl mixin 🧩 Mixin 配置管理
clashctl relay 🔗 多跳节点管理
clashctl sub 🧩 订阅高级管理(启用 / 禁用 / 重命名 / 删除)
clashctl upgrade 🚀 升级当前或指定内核
clashctl update 🔄 更新项目代码
clash lan 🏠 局域网代理管理
clash tun 🧪 Tun 模式管理(需root方式安装)
clash boot 🚦 开机代理接管管理
clash mixin 🧩 Mixin 配置管理
clash relay 🔗 多跳节点管理
clash sub 🧩 订阅高级管理(启用 / 禁用 / 重命名 / 删除)
clash upgrade 🚀 升级当前或指定内核
clash update 🔄 更新项目代码
📜 日志
clashctl doctor 🩺 诊断面板
clashctl log/logs 📜 查看日志
clash doctor 🩺 诊断面板
clash log/logs 📜 查看日志
💡 显示更多帮助命令:clashctl -h
💡 显示更多帮助命令:clash -h
```
`clashctl` 旧入口继续保留兼容;新文档、帮助和终端提示统一使用更简洁的 `clash`。
------
## 🧭 路由模式与节点连通性
```bash
clashctl mode
clashctl mode global
clashctl mode rule
clashctl mode direct
clash mode
clash mode global
clash mode rule
clash mode direct
clashctl test
clash test
clashtest
clashtest "节点选择"
```
- `clashctl mode` 查看当前的 Clash 路由模式,支持切换 `rule`、`global`、`direct`。
- `global` 模式使用 `GLOBAL` 策略组当前选择的策略组或节点;如需调整可执行 `clashctl select`。
- `clashctl test` 与 `clashtest` 等价,默认测试当前路由模式对应的策略组当前选择能否访问 Google 和 YouTube,并显示延迟。
- `clash mode` 查看当前的 Clash 路由模式,支持切换 `rule`、`global`、`direct`。
- `global` 模式使用 `GLOBAL` 策略组当前选择的策略组或节点;如需调整可执行 `clash select`。
- `clash test` 与 `clashtest` 等价,默认测试当前路由模式对应的策略组当前选择能否访问 Google 和 YouTube,并显示延迟。
- 连通性测试不会切换节点;任一目标不可访问时命令返回非零状态,便于脚本判断。
------
@@ -134,7 +136,7 @@ $ clashsecret
```
- 可通过浏览器打开 `Web` 控制台进行可视化操作,例如切换节点、查看日志等。
- `clashctl secret` 与 `clashsecret` 都支持无参数查看、有参数直接设置。
- `clash secret` 与 `clashsecret` 都支持无参数查看、有参数直接设置。
- 默认使用 [zashboard](https://github.com/Zephyruso/zashboard) 作为控制台前端,如需更换可自行配置。
- 控制台中的“浏览器出口 IP”由访问面板的浏览器发起查询,可能与远端 Mihomo 的代理出口不同;远端出口请在服务器上通过本地代理端口查询。
- 运行配置会把 `external-ui` 指向本地 Dashboard 目录,并把 `external-ui-url` 指向 zashboard 的 `dist.zip` 下载地址;面板内 `/upgrade/ui` 会使用该地址更新前端。
@@ -147,32 +149,32 @@ $ clashsecret
项目默认开启局域网代理:运行配置会写入 `allow-lan: true`,并把 `external-controller` 绑定到 `0.0.0.0`,避免订阅文件里的 `allow-lan: false` 覆盖项目默认值。
```bash
clashctl lan status
clashctl lan on
clashctl lan off
clash lan status
clash lan on
clash lan off
```
开启后,同一局域网设备可把 HTTP / SOCKS 代理地址设置为 `http://<本机局域网IP>:<mixed-port>`,端口默认是 `7890`;如访问不了,请检查系统防火墙是否放行该端口。
------
OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
OpenWrt 下 root/system 安装会把 `clash`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
## 🧰 常用管理命令
### 多订阅管理
```
clashctl add <订阅链接> <名称>
clashctl use
clashctl ls
clashctl sub update
clash add <订阅链接> <名称>
clash use
clash ls
clash sub update
clashsub update # 兼容旧版命令
clashctl sub
clashctl sub list
clashctl sub enable <名称>
clashctl sub disable <名称>
clashctl sub rename <旧名称> <新名称>
clashctl sub remove <名称>
clash sub
clash sub list
clash sub enable <名称>
clash sub disable <名称>
clash sub rename <旧名称> <新名称>
clash sub remove <名称>
```
WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降级:运行时照常启动,当前 Shell 代理变量生效;系统代理持久接管和开机代理保持不可用。
@@ -182,20 +184,20 @@ WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降
推荐使用交互导入,放置目录为:`$PROJECT_DIR/runtime/subscriptions/`
```bash
clashctl add local
clash add local
# 输入:clash.yaml
```
实际等价于:
```bash
clashctl add "file://$PROJECT_DIR/runtime/subscriptions/clash.yaml"
clash add "file://$PROJECT_DIR/runtime/subscriptions/clash.yaml"
```
进阶用法:也可以直接使用 `file://` 绝对路径导入:
```bash
clashctl add "file:///绝对路径/clash.yaml" home
clash add "file:///绝对路径/clash.yaml" home
```
支持格式:
@@ -207,11 +209,11 @@ clashctl add "file:///绝对路径/clash.yaml" home
### 开机接管(内核 + 代理)
```bash
clashctl boot status
clashctl boot on
clashctl boot off
clashctl boot runtime on|off|status
clashctl boot proxy on|off|status
clash boot status
clash boot on
clash boot off
clash boot runtime on|off|status
clash boot proxy on|off|status
```
- `boot runtime`:只管理内核开机自启(systemd / systemd-user 可用,script 后端为 `unsupported`)。
@@ -244,11 +246,11 @@ opkg install bash curl tar gzip coreutils-readlink unzip
```bash
bash install.sh
clashon
clashctl status
clash status
clashoff
```
OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
OpenWrt 下 root/system 安装会把 `clash`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
## 🏗️ 架构设计架构简述
@@ -258,7 +260,7 @@ OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命
用户入口层。
- `clashctl`
- `clash`
- `clashon`
- `clashoff`
- `status`
@@ -341,13 +343,13 @@ CLASH_GH_PROXY=https://ghfast.top
```
可用镜像列表参考:<https://ghproxy.link/>
当前镜像使用状态可通过 `clashctl doctor` 查看。
当前镜像使用状态可通过 `clash doctor` 查看。
### 内置运行依赖
当前正式支持的架构为 `amd64`、`arm64`、`armv7`。超出这三种架构时会明确失败,不会伪装成已支持。
如果安装环境访问 GitHub 很慢,可以把 Mihomo、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clashctl upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;本地没有对应文件时,会回退到原来的远程下载逻辑,不影响后续升级内核。
如果安装环境访问 GitHub 很慢,可以把 Mihomo、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clash upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;本地没有对应文件时,会回退到原来的远程下载逻辑,不影响后续升级内核。
Clash 仅作为兼容内核处理,固定走远程下载,不会命中 `resources/bin` 中的本地资源。
@@ -381,19 +383,19 @@ resources/geo/Country.mmdb
查看当前模板:
```bash
clashctl mixin
clash mixin
```
编辑:
```bash
clashctl mixin edit
clash mixin edit
```
查看最终运行配置:
```bash
clashctl mixin runtime
clash mixin runtime
```
###
@@ -403,10 +405,10 @@ clashctl mixin runtime
## 🔄 更新
```bash
clashctl update
clashctl upgrade
clashctl upgrade mihomo
clashctl upgrade clash
clash update
clash upgrade
clash upgrade mihomo
clash upgrade clash
```
`update` 用于更新项目代码与运行依赖。`upgrade` 用于升级当前或指定代理内核。
@@ -416,10 +418,10 @@ clashctl upgrade clash
## 🧩 Mixin 配置
```bash
clashctl mixin
clashctl mixin edit
clashctl mixin raw
clashctl mixin runtime
clash mixin
clash mixin edit
clash mixin raw
clash mixin runtime
```
Mixin 是运行配置补丁,不是订阅管理。它优先通过 `runtime/mixin.yaml`(兼容读取 `config/mixin.yaml`)对当前 active 订阅生成的运行配置执行:
@@ -451,7 +453,7 @@ append:
编辑后执行:
```bash
clashctl mixin edit
clash mixin edit
```
它会重新生成配置;如果代理正在运行,会自动重启应用。
@@ -468,8 +470,8 @@ clashui
按域名小范围测试:
```bash
clashctl relay add 多跳-示例 节点A 节点B --domain example.com
clashctl relay list
clash relay add 多跳-示例 节点A 节点B --domain example.com
clash relay list
```
也可以使用快捷入口:`clashrelay list`。
@@ -477,13 +479,13 @@ clashctl relay list
全局接管:
```bash
clashctl relay add 全局多跳 节点A 节点B --match
clash relay add 全局多跳 节点A 节点B --match
```
`--match` 会让所有未提前命中的流量走多跳,建议先用 `--domain` 验证链路。删除多跳配置:
```bash
clashctl relay remove 多跳-示例
clash relay remove 多跳-示例
```
------
@@ -491,22 +493,22 @@ clashctl relay remove 多跳-示例
## 🌐 Tun 模式
```bash
clashctl tun on
clashctl tun off
clashctl tun on-proxy-off
clashctl tun off-proxy-on
clashctl tun doctor
clashctl tun logs
clash tun on
clash tun off
clash tun on-proxy-off
clash tun off-proxy-on
clash tun doctor
clash tun logs
```
Tun 用于透明接管链路。`tun on` 只负责开启 Tun,不会自动关闭系统代理;如需切换到 Tun 接管并关闭系统代理,使用 `clashctl tun on-proxy-off`。如需关闭 Tun 并恢复普通系统代理模式,使用 `clashctl tun off-proxy-on`。
Tun 用于透明接管链路。`tun on` 只负责开启 Tun,不会自动关闭系统代理;如需切换到 Tun 接管并关闭系统代理,使用 `clash tun on-proxy-off`。如需关闭 Tun 并恢复普通系统代理模式,使用 `clash tun off-proxy-on`。
`clashctl doctor` 会检查 Tun 与系统代理是否同时开启;如果同时开启,会提示 `clashctl tun on-proxy-off`,避免流量接管路径重复或排障混淆。
`clash doctor` 会检查 Tun 与系统代理是否同时开启;如果同时开启,会提示 `clash tun on-proxy-off`,避免流量接管路径重复或排障混淆。
`tun on` 是动作反馈,展示当前关键配置和简短状态;完整证据请看:
```bash
clashctl tun doctor
clash tun doctor
```
Tun 判断不会简单把 `root` 等同于拥有 `CAP_NET_ADMIN`,也不会把 main table 默认路由未切换直接等同于 Tun 未生效。诊断会结合运行后端、容器环境、进程能力、Tun adapter、policy routing、路由表和日志证据。
@@ -519,7 +521,7 @@ Tun 判断不会简单把 `root` 等同于拥有 `CAP_NET_ADMIN`,也不会把
bash uninstall.sh
```
默认执行完整卸载:停止 mihomo/subconverter,关闭系统代理持久接管,删除 systemd/脚本入口、`clashctl`、命令补全、shell alias、shell proxy 持久状态、controller secret 和运行目录。完整卸载完成后,脚本会提示其他卸载方式。
默认执行完整卸载:停止 mihomo/subconverter,关闭系统代理持久接管,删除 systemd/脚本入口、`clash`、命令补全、shell alias、shell proxy 持久状态、controller secret 和运行目录。完整卸载完成后,脚本会提示其他卸载方式。
`--purge-runtime` 保留为兼容别名;默认卸载已经会清理运行目录。
+1 -1
View File
@@ -65,7 +65,7 @@ if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
if [ -f "$RUNTIME_DIR/tmp/subscription-invalid-preview.txt" ]; then
echo "🧾 调试预览:$RUNTIME_DIR/tmp/subscription-invalid-preview.txt"
fi
echo "👉 下一步:clashctl doctor"
echo "👉 下一步:clash doctor"
echo
exit 1
fi
+1 -1
View File
@@ -1,6 +1,6 @@
# 内置运行依赖
如果安装时 GitHub 下载太慢,可以把 Mihomo、yq、subconverter 提前放到本目录。安装和 `clashctl upgrade` 会优先读取这里的文件;没有匹配文件时,仍会回退到原来的下载逻辑。
如果安装时 GitHub 下载太慢,可以把 Mihomo、yq、subconverter 提前放到本目录。安装和 `clash upgrade` 会优先读取这里的文件;没有匹配文件时,仍会回退到原来的下载逻辑。
Clash 是兼容内核,固定走远程下载,不使用本目录中的本地资源。
+40 -30
View File
@@ -216,12 +216,12 @@ _clash_alias_unset_shell_proxy() {
}
_clash_alias_status_next() {
_clashctl_real status-next 2>/dev/null || echo "clashctl status"
_clashctl_real status-next 2>/dev/null || echo "clash status"
}
_clash_alias_prepare_on() {
# 这里不直接自己做 regenerate / restart / fallback,
# 而是统一交给 clashctl on 主链处理,避免双执行链。
# 而是统一交给 clash on 主链处理,避免双执行链。
# shell 层只负责“闭环体验”,不抢 runtime / build 的职责。
return 0
}
@@ -258,12 +258,12 @@ _clash_alias_run_on() {
if [ "$on_rc" -ne 0 ]; then
if _clash_alias_export_system_proxy; then
echo "🚨 clashctl on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
echo "🚨 clash on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
fi
elif _clash_alias_proxy_on_system; then
echo "🚨 clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
echo "🚨 clash on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2
fi
@@ -304,7 +304,7 @@ _clash_alias_auto_restore_proxy() {
return 0
}
clashctl() {
_clash_command() {
case "${1:-}" in
on)
shift || true
@@ -349,101 +349,111 @@ clashctl() {
esac
}
# 快捷入口全部收敛到 clashctl 函数
clashctl() {
_clash_command "$@"
}
if [ "${CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED:-false}" = "true" ]; then
clash() {
_clash_command "$@"
}
fi
# 快捷入口全部收敛到统一命令函数
clashon() {
clashctl on "$@" || return $?
_clash_command on "$@" || return $?
}
clashoff() {
clashctl off "$@" || return $?
_clash_command off "$@" || return $?
}
clashproxy() {
case "${1:-show}" in
on)
clashctl proxy on
_clash_command proxy on
;;
off)
clashctl proxy off
_clash_command proxy off
;;
show|status)
clashctl proxy show
_clash_command proxy show
;;
groups)
clashctl proxy groups
_clash_command proxy groups
;;
current)
shift || true
clashctl proxy current "$@"
_clash_command proxy current "$@"
;;
nodes)
shift || true
clashctl proxy nodes "$@"
_clash_command proxy nodes "$@"
;;
select)
shift || true
clashctl proxy select "$@"
_clash_command proxy select "$@"
;;
*)
echo "📜 用法:clashproxy [show|on|off|groups|current|nodes|select]"
echo "💡 主路径切节点请使用:clashselect 或 clashctl select"
echo "💡 主路径切节点请使用:clashselect 或 clash select"
return 2
;;
esac
}
clashls() {
clashctl ls "$@"
_clash_command ls "$@"
}
clashsub() {
clashctl sub "$@"
_clash_command sub "$@"
}
clashselect() {
clashctl select "$@"
_clash_command select "$@"
}
clashtest() {
clashctl test "$@"
_clash_command test "$@"
}
clashui() {
clashctl ui "$@"
_clash_command ui "$@"
}
clashsecret() {
clashctl secret "$@"
_clash_command secret "$@"
}
clashtun() {
clashctl tun "$@"
_clash_command tun "$@"
}
clashrelay() {
clashctl relay "$@"
_clash_command relay "$@"
}
clashupgrade() {
clashctl upgrade "$@"
_clash_command upgrade "$@"
}
clashmixin() {
case "${1:-}" in
-e|--edit)
clashctl mixin edit
_clash_command mixin edit
;;
-c|--raw)
clashctl mixin raw
_clash_command mixin raw
;;
-r|--runtime)
clashctl mixin runtime
_clash_command mixin runtime
;;
"")
clashctl mixin
_clash_command mixin
;;
*)
clashctl mixin "$@"
_clash_command mixin "$@"
;;
esac
}
+320 -314
View File
File diff suppressed because it is too large Load Diff
+75 -18
View File
@@ -243,23 +243,23 @@ install_build_result_text() {
install_next_step_text() {
case "$(install_state_text)" in
ready)
echo "clashctl select"
echo "clash select"
;;
verifying)
if install_has_subscription; then
echo "clashon"
else
echo "clashctl add <订阅链接>"
echo "clash add <订阅链接>"
fi
;;
broken)
echo "clashctl doctor"
echo "clash doctor"
;;
stopped)
echo "clashctl add <订阅链接>"
echo "clash add <订阅链接>"
;;
*)
echo "clashctl status"
echo "clash status"
;;
esac
}
@@ -951,7 +951,7 @@ download_file() {
rm -f "$fetch_tmp" 2>/dev/null || true
die_state "下载失败:${asset_name}" \
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor"
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clash doctor"
fi
ordered_entries="$(
@@ -1012,7 +1012,7 @@ EOF
rm -f "$fetch_tmp" 2>/dev/null || true
die_state "下载失败:${asset_name}" \
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor"
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clash doctor"
}
download_text_tmp_file() {
@@ -2103,7 +2103,7 @@ guard_sudo_on_user_install() {
echo "❗ 操作被拒绝:user 安装模式不支持以 sudo 运行"
echo "🚨 原因:sudo 会将 runtime 文件写成 root:root,导致 systemctl --user 无法访问"
echo "👤 请以安装用户(${SUDO_USER})直接执行:"
echo " clashctl tun ${_action}"
echo " clash tun ${_action}"
echo
return 1
}
@@ -2535,6 +2535,51 @@ clashctl_bin_entry_target() {
echo "$(command_install_dir)/clashctl-bin"
}
clash_entry_target() {
echo "$(command_install_dir)/clash"
}
clash_entry_marker() {
echo "# clash-for-linux managed command: clash"
}
clash_entry_is_managed() {
local target="${1:-}"
[ -n "${target:-}" ] || target="$(clash_entry_target)"
[ -f "$target" ] || return 1
grep -Fqx "$(clash_entry_marker)" "$target" 2>/dev/null
}
install_clash_entry() {
local target bin_target existing_command=""
target="$(clash_entry_target)"
bin_target="$(clashctl_bin_entry_target)"
if [ -e "$target" ] || [ -L "$target" ]; then
if ! clash_entry_is_managed "$target"; then
existing_command="$target"
warn "检测到已有 clash 命令,未覆盖:$existing_command"
warn "本项目仍可使用 clashctl;如需 clash 入口,请先自行处理同名命令"
return 0
fi
elif command -v clash >/dev/null 2>&1; then
existing_command="$(command -v clash 2>/dev/null || true)"
warn "检测到已有 clash 命令,未覆盖:$existing_command"
warn "本项目仍可使用 clashctl;如需 clash 入口,请先自行处理同名命令"
return 0
fi
cat > "$target" <<EOF
#!/usr/bin/env bash
$(clash_entry_marker)
set -euo pipefail
exec "$bin_target" "\$@"
EOF
chmod +x "$target"
}
ensure_command_install_dir_in_shell_path() {
local install_dir shell_rc home_dir
@@ -2582,6 +2627,7 @@ exec "$bin_target" "\$@"
EOF
chmod +x "$target"
install_clash_entry
ensure_command_install_dir_in_shell_path
}
@@ -2635,7 +2681,7 @@ cleanup_legacy_shell_entries() {
}
install_shell_alias_entry() {
local profile_file alias_file shell_rc bash_completion_file zsh_completion_file home_dir system_zsh_rc
local profile_file alias_file shell_rc bash_completion_file zsh_completion_file home_dir system_zsh_rc clash_entry_managed
cleanup_legacy_shell_entries
@@ -2643,6 +2689,8 @@ install_shell_alias_entry() {
alias_file="$(alias_source_file)"
bash_completion_file="$(bash_completion_entry_file)"
zsh_completion_file="$(zsh_completion_entry_file)"
clash_entry_managed="false"
clash_entry_is_managed && clash_entry_managed="true"
mkdir -p "$(dirname "$profile_file")"
[ -f "$alias_file" ] || die "未找到 alias 脚本:$alias_file"
@@ -2651,6 +2699,7 @@ cat > "$profile_file" <<EOF
#!/usr/bin/env bash
# clash-for-linux shell entry
export PATH="$(command_install_dir):\$PATH"
export CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED="$clash_entry_managed"
if [ -z "\${CLASH_FOR_LINUX_SHELL_LOADED:-}" ] \
&& [ -r "$alias_file" ] \
@@ -2705,6 +2754,9 @@ install_clashctl_completion() {
}
remove_clashctl_entry() {
if clash_entry_is_managed; then
rm -f "$(clash_entry_target)" 2>/dev/null || true
fi
rm -f "$(clashctl_entry_target)" "$(clashctl_bin_entry_target)" 2>/dev/null || true
remove_alias_command_wrappers
}
@@ -3054,31 +3106,31 @@ install_status_label() {
install_default_next_action() {
case "$(install_status_text)" in
ready)
echo "clashctl select"
echo "clash select"
;;
stopped)
if install_has_subscription; then
echo "clashon"
else
echo "clashctl add <订阅链接>"
echo "clash add <订阅链接>"
fi
;;
verifying)
if status_is_running 2>/dev/null; then
echo "clashctl status"
echo "clash status"
else
echo "clashon"
fi
;;
broken)
if install_has_subscription; then
echo "clashctl doctor"
echo "clash doctor"
else
echo "clashctl add <订阅链接>"
echo "clash add <订阅链接>"
fi
;;
*)
echo "clashctl status"
echo "clash status"
;;
esac
}
@@ -3202,6 +3254,11 @@ print_install_summary() {
echo "💻 环境模式:${env_mode_text:-unknown}"
echo "📁 安装路径:$project_path"
echo "👤 安装方式:${install_actor} / ${install_scope_text:-unknown}"
if clash_entry_is_managed; then
echo "⌨️ 主命令:clash"
else
echo "⚠️ 主命令:clash 同名冲突,未由本项目接管(兼容入口 clashctl 仍可用)"
fi
echo "🔧 运行后端:${backend_text:-unknown}"
echo "📦 订阅:$subscription_text"
[ -n "${node_count:-}" ] && echo "🔢 节点数量:$node_count"
@@ -3211,8 +3268,8 @@ print_install_summary() {
echo "🚫 安装后验证:代理端口绑定被系统拒绝(非端口冲突)"
echo "📌 这通常是环境限制,修改端口通常无法解决"
install_mixed_port_bind_observation_line 2>/dev/null || true
echo "👉 下一步:clashctl doctor"
echo "👉 日志:clashctl logs mihomo"
echo "👉 下一步:clash doctor"
echo "👉 日志:clash logs mihomo"
;;
address_in_use)
:
@@ -3225,5 +3282,5 @@ print_install_summary() {
echo
echo "💡 clashon / clashoff 为 shell 快捷入口;新终端会自动生效"
echo "💡 当前终端若暂不可用,请先使用 clashctl on / clashctl off"
echo "💡 当前终端若暂不可用,请先使用 clash on / clash off"
}
+5 -2
View File
@@ -360,7 +360,7 @@ _clash_for_linux_complete_command() {
cur="${COMP_WORDS[COMP_CWORD]:-}"
case "$root" in
clashctl)
clash|clashctl)
if [ "$COMP_CWORD" -eq 1 ]; then
_clash_for_linux_complete_top_level "$cur"
return 0
@@ -441,6 +441,9 @@ _clash_for_linux_complete_command() {
esac
}
if [ "${CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED:-false}" = "true" ]; then
complete -F _clash_for_linux_complete_command clash
fi
complete -F _clash_for_linux_complete_command clashctl
complete -F _clash_for_linux_complete_command clashtest
complete -F _clash_for_linux_complete_command clashsub
@@ -477,7 +480,7 @@ cmd_completion() {
completion_emit_zsh_script
;;
*)
die_usage "completion 参数不合法" "clashctl completion bash|zsh"
die_usage "completion 参数不合法" "clash completion bash|zsh"
;;
esac
}
+20 -20
View File
@@ -119,7 +119,7 @@ http_code : $http_code"
detail="${detail}
process_status : $process_status"
detail="${detail}
next_step : clashctl doctor;查看 subconverter_log 定位转换失败原因"
next_step : clash doctor;查看 subconverter_log 定位转换失败原因"
SUBCONVERTER_LAST_ERROR_DETAIL="$detail"
SUBCONVERTER_LAST_ERROR_SUMMARY="subconverter 转换失败:$failure_type"
@@ -1165,8 +1165,8 @@ print_subscription_health_one() {
local name="$1"
local status fail_count last_success last_failure last_error auto_disabled auto_disabled_at enabled_text type_text url_text
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clashctl health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls"
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clash health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
status="$(read_subscription_health_value "$name" "STATUS" 2>/dev/null || echo "unknown")"
fail_count="$(read_subscription_health_value "$name" "FAIL_COUNT" 2>/dev/null || echo "0")"
@@ -1310,7 +1310,7 @@ set_subscription_enabled() {
file="$(subscriptions_file)"
ensure_subscriptions_file
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls"
subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
case "$enabled" in
true|false) ;;
@@ -1417,7 +1417,7 @@ print_build_explain() {
esac
echo
ui_next "clashctl status --verbose"
ui_next "clash status --verbose"
ui_blank
}
@@ -1519,16 +1519,16 @@ build_explain_next_steps() {
failed_csv="$(build_failed_active_sources_csv)"
if [ "${last_status:-}" = "success" ]; then
echo "👉 clashctl health"
echo "👉 clashctl select"
echo "👉 clash health"
echo "👉 clash select"
return 0
fi
if [ -n "${failed_csv:-}" ]; then
echo "👉 clashctl health"
echo "👉 clash health"
fi
echo "👉 clashctl doctor"
echo "👉 clash doctor"
}
runtime_config_file() {
@@ -2484,7 +2484,7 @@ set_active_subscription() {
exists="$("$(yq_bin)" eval ".sources | has(\"$name\")" "$file")"
[ "$exists" = "true" ] || die_missing "订阅 $name" "clashctl ls"
[ "$exists" = "true" ] || die_missing "订阅 $name" "clash ls"
NAME="$name" "$(yq_bin)" eval -i '
.active = env(NAME)
@@ -2500,8 +2500,8 @@ remove_subscription() {
file="$(subscriptions_file)"
ensure_subscriptions_file
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clashctl health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls"
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clash health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
if [ "$name" = "default" ]; then
die "默认订阅不允许删除:default"
@@ -2541,8 +2541,8 @@ rename_subscription() {
[ -n "${old_name:-}" ] || die "原订阅名称不能为空"
[ -n "${new_name:-}" ] || die "新订阅名称不能为空"
subscription_exists "$old_name" || die_missing "订阅 $old_name" "clashctl ls"
subscription_exists "$new_name" && die_state "目标订阅已存在:$new_name" "clashctl ls"
subscription_exists "$old_name" || die_missing "订阅 $old_name" "clash ls"
subscription_exists "$new_name" && die_state "目标订阅已存在:$new_name" "clash ls"
case "$new_name" in
*[!A-Za-z0-9._-]*)
@@ -3039,7 +3039,7 @@ subscription_list_overview_lines() {
}
subscription_list_recommendation_lines() {
echo "👉 clashctl use 切换当前使用的订阅"
echo "👉 clash use 切换当前使用的订阅"
}
detect_subscription_format() {
@@ -3076,7 +3076,7 @@ prompt_subscription_if_needed() {
if ! subscription_url_is_supported "$input_url"; then
ui_warn "订阅链接格式不合法,已跳过"
ui_next "稍后执行:clashctl add <订阅链接>"
ui_next "稍后执行:clash add <订阅链接>"
return 0
fi
@@ -3221,7 +3221,7 @@ start_subconverter() {
fi
error "subconverter 启动失败"
warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常"
warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
return 1
}
@@ -3618,7 +3618,7 @@ convert_subscription_via_subconverter() {
"${http_code:-unknown}" \
"$process_status"
warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常"
warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" "$curl_error_file" 2>/dev/null || true
return 1
fi
@@ -3638,7 +3638,7 @@ convert_subscription_via_subconverter() {
"$process_status"
warn "订阅转换失败:subconverter 返回内容为空"
warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常"
warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" 2>/dev/null || true
return 1
}
@@ -3675,7 +3675,7 @@ convert_subscription_via_subconverter() {
warn "订阅转换失败:subconverter 返回内容不是合法 Clash YAML"
fi
warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常"
warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" 2>/dev/null || true
return 1
+1 -1
View File
@@ -204,7 +204,7 @@ update_project_code() {
fi
if git_has_local_changes && [ "$force_mode" != "true" ]; then
die "检测到本地有未提交改动,请先提交,或使用 clashctl update --force 强制覆盖"
die "检测到本地有未提交改动,请先提交,或使用 clash update --force 强制覆盖"
fi
info "正在获取最新代码"
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=../core/common.sh
source "$PROJECT_DIR/scripts/core/common.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
fixture_bin="$tmp_dir/bin"
mkdir -p "$fixture_bin"
command_install_dir() { printf '%s\n' "$fixture_bin"; }
ensure_command_install_dir_in_shell_path() { :; }
remove_alias_command_wrappers() { :; }
install_clashctl_entry
for command_name in clash clashctl clashctl-bin; do
if [ ! -x "$fixture_bin/$command_name" ]; then
echo "not ok - install should create $command_name" >&2
exit 1
fi
done
primary_help="$("$fixture_bin/clash" --help)"
compat_help="$("$fixture_bin/clashctl" --help)"
if ! printf '%s\n' "$primary_help" | grep -Fq "clash <command>"; then
echo "not ok - clash help should use the primary command name" >&2
exit 1
fi
if [ "$compat_help" != "$primary_help" ]; then
echo "not ok - clashctl should remain compatible with clash" >&2
exit 1
fi
completion_output="$(
"$fixture_bin/clash" completion bash
)"
if ! printf '%s\n' "$completion_output" | grep -Eq '^[[:space:]]*complete -F _clash_for_linux_complete_command clash$'; then
echo "not ok - completion should register the clash command" >&2
exit 1
fi
primary_completion="$(
bash --noprofile --norc -c '
CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED=true
source "$1"
COMP_WORDS=(clash st)
COMP_CWORD=1
_clash_for_linux_complete_command
printf "%s\n" "${COMPREPLY[@]}"
' bash <(printf '%s\n' "$completion_output")
)"
if ! printf '%s\n' "$primary_completion" | grep -Fxq status; then
echo "not ok - clash should complete top-level commands" >&2
exit 1
fi
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*"' > "$fixture_bin/clashctl-bin"
chmod +x "$fixture_bin/clashctl-bin"
shell_output="$(
PATH="$fixture_bin:/usr/bin:/bin" \
CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED=true \
bash --noprofile --norc -c \
'source "$1"; clash status' \
bash "$PROJECT_DIR/scripts/core/alias.sh"
)"
if [ "$shell_output" != "status" ]; then
echo "not ok - clash shell function should delegate to the shared command path" >&2
printf 'actual: %s\n' "$shell_output" >&2
exit 1
fi
remove_clashctl_entry
for command_name in clash clashctl clashctl-bin; do
if [ -e "$fixture_bin/$command_name" ]; then
echo "not ok - uninstall should remove managed $command_name" >&2
exit 1
fi
done
printf '%s\n' '#!/usr/bin/env bash' 'echo foreign-clash' > "$fixture_bin/clash"
chmod +x "$fixture_bin/clash"
install_clashctl_entry >/dev/null 2>&1
if [ "$("$fixture_bin/clash")" != "foreign-clash" ]; then
echo "not ok - install should not overwrite an existing foreign clash command" >&2
exit 1
fi
remove_clashctl_entry
if [ "$("$fixture_bin/clash")" != "foreign-clash" ]; then
echo "not ok - uninstall should preserve an existing foreign clash command" >&2
exit 1
fi
other_bin="$tmp_dir/other-bin"
foreign_bin="$tmp_dir/foreign-bin"
mkdir -p "$other_bin" "$foreign_bin"
printf '%s\n' '#!/usr/bin/env bash' 'echo foreign-path-clash' > "$foreign_bin/clash"
chmod +x "$foreign_bin/clash"
command_install_dir() { printf '%s\n' "$other_bin"; }
PATH="$foreign_bin:/usr/bin:/bin" install_clashctl_entry >/dev/null 2>&1
if [ -e "$other_bin/clash" ]; then
echo "not ok - install should not shadow a foreign clash command elsewhere in PATH" >&2
exit 1
fi
remove_clashctl_entry
echo "ok - clash is primary, clashctl stays compatible, and foreign commands are preserved"
+1 -1
View File
@@ -41,7 +41,7 @@ run_help_case() {
return 1
fi
if ! printf '%s\n' "$output" | grep -Fq "clashctl select <策略组> <节点>"; then
if ! printf '%s\n' "$output" | grep -Fq "clash select <策略组> <节点>"; then
echo "not ok - select help missing direct usage" >&2
printf '%s\n' "$output" >&2
return 1
+7 -6
View File
@@ -302,8 +302,9 @@ wait_install_runtime_ready() {
write_runtime_value "INSTALL_VERIFY_CONTROLLER_READY" "$controller_ready"
}
clashctl_command_available() {
[ -x "$(clashctl_entry_target)" ] \
clash_command_available() {
clash_entry_is_managed \
|| [ -x "$(clashctl_entry_target)" ] \
|| [ -x "$(clashctl_bin_entry_target)" ] \
|| command -v clashctl >/dev/null 2>&1
}
@@ -314,12 +315,12 @@ post_install_verify() {
local controller_ready="false"
local bind_failure_kind="" bind_failure_port=""
if clashctl_command_available; then
if clash_command_available; then
write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "true"
else
write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "false"
write_runtime_event_value "RUNTIME_LAST_INSTALL_READY" "false"
die "clashctl 安装失败,命令不可用"
die "clash 管理命令安装失败,命令不可用"
fi
if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
@@ -357,8 +358,8 @@ post_install_verify() {
echo "🚫 安装后验证发现:代理端口 ${bind_failure_port:-unknown} 绑定被系统拒绝(非端口冲突)"
echo "📌 修改端口通常无法解决;install 阶段不会通过自动换端口处理该问题"
install_mixed_port_bind_observation_line 2>/dev/null || true
echo "👉 下一步:clashctl doctor"
echo "👉 日志:clashctl logs mihomo"
echo "👉 下一步:clash doctor"
echo "👉 日志:clash logs mihomo"
echo
fi