Update clashctl

This commit is contained in:
wnlen
2026-03-20 14:57:41 +08:00
parent 0597533b9a
commit 4ae8d972fe

520
clashctl
View File

@ -1,305 +1,285 @@
#!/bin/bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) Server_Dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVICE_NAME="clash-for-linux" Service_Name="clash-for-linux.service"
PROFILED_FILE="/etc/profile.d/clash-for-linux.sh"
ENV_FILE="$Server_Dir/.env"
CONF_FILE="$Server_Dir/conf/config.yaml"
TEMP_CONF_FILE="$Server_Dir/temp/config.yaml"
resolve_clash_home() { log() { printf "%b\n" "$*"; }
local candidates info() { log "\033[36m[INFO]\033[0m $*"; }
if [ -n "${CLASH_HOME:-}" ] && [ -d "$CLASH_HOME" ]; then ok() { log "\033[32m[OK]\033[0m $*"; }
echo "$CLASH_HOME" warn() { log "\033[33m[WARN]\033[0m $*"; }
return 0 err() { log "\033[31m[ERROR]\033[0m $*"; }
fi
if [ -f "/etc/default/${SERVICE_NAME}" ]; then
candidates=$(awk -F= '/^CLASH_HOME=/{print $2}' "/etc/default/${SERVICE_NAME}" | tr -d '"')
if [ -n "$candidates" ] && [ -d "$candidates" ]; then
echo "$candidates"
return 0
fi
fi
if [ -f "$SCRIPT_DIR/.env" ]; then
echo "$SCRIPT_DIR"
return 0
fi
if [ -f "/opt/clash-for-linux/.env" ]; then
echo "/opt/clash-for-linux"
return 0
fi
echo "$SCRIPT_DIR"
}
CLASH_HOME=$(resolve_clash_home) usage() {
ENV_FILE="$CLASH_HOME/.env" cat <<'EOF'
PID_FILE="$CLASH_HOME/temp/clash.pid" Usage:
SUBSCRIPTION_FILE="$CLASH_HOME/conf/subscriptions.list" clashctl COMMAND [OPTIONS]
use_systemd() {
command -v systemctl >/dev/null 2>&1 || return 1
systemctl show --property=Version --value >/dev/null 2>&1 || return 1
return 0
}
action_with_systemd() {
local action="$1"
if use_systemd; then
if systemctl "$action" "${SERVICE_NAME}.service" >/dev/null 2>&1; then
return 0
fi
fi
return 1
}
print_usage() {
cat <<USAGE
Usage: clashctl <command> [args]
Commands: Commands:
start Start Clash service on 开启代理
stop Stop Clash service off 关闭代理
restart Restart Clash service status 内核状况
status Show Clash service status proxy 系统代理
update Refresh subscription config ui Web 面板
set-url <url> Update CLASH_URL in .env secret Web 密钥
sub add <name> <url> [headers] Add subscription entry sub 订阅管理
sub del <name> Delete subscription entry upgrade 升级内核
sub use <name> Activate subscription entry tun Tun 模式
sub update [name] Update subscription config by entry mixin Mixin 配置
sub list List subscriptions
sub log Show subscription update logs
Environment: Global Options:
CLASH_HOME Override Clash installation directory -h, --help 显示帮助信息
USAGE
Examples:
clashctl on
clashctl off
clashctl status
clashctl proxy status
clashctl ui
clashctl secret
clashctl sub show
clashctl sub update
clashctl tun status
clashctl mixin status
💡 clashon 同 clashctl onTab 补全更方便!
EOF
} }
set_env_var() { require_profiled() {
local key="$1" if [ ! -f "$PROFILED_FILE" ]; then
local value="${2:-}" err "未安装 Shell 代理快捷命令:$PROFILED_FILE"
if [ ! -f "$ENV_FILE" ]; then exit 1
echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2 fi
exit 1
fi
local escaped escaped_sed
escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g")
escaped_sed=$(printf "%s" "$escaped" | sed 's/[\\&@]/\\&/g')
if grep -q "^export ${key}=" "$ENV_FILE"; then
sed -i "s@^export ${key}=.*@export ${key}='${escaped_sed}'@" "$ENV_FILE"
else
echo "export ${key}='${escaped}'" >> "$ENV_FILE"
fi
} }
set_url() { has_systemd() {
local url="$1" command -v systemctl >/dev/null 2>&1
if [ -z "$url" ]; then
echo "[ERROR] 请提供订阅地址" >&2
exit 1
fi
set_env_var "CLASH_URL" "$url"
set_env_var "CLASH_SUBSCRIPTION" ""
echo "[OK] 已更新 CLASH_URL"
} }
ensure_subscription_file() { service_exists() {
mkdir -p "$(dirname "$SUBSCRIPTION_FILE")" has_systemd && systemctl list-unit-files 2>/dev/null | grep -q "^${Service_Name}"
if [ ! -f "$SUBSCRIPTION_FILE" ]; then
touch "$SUBSCRIPTION_FILE"
fi
} }
subscription_lookup() { read_config_value() {
local name="$1" local key="$1"
awk -F'|' -v target="$name" '$1 == target {print; exit}' "$SUBSCRIPTION_FILE" local f
for f in "$TEMP_CONF_FILE" "$CONF_FILE"; do
if [ -f "$f" ]; then
sed -nE "s/^[[:space:]]*${key}:[[:space:]]*//p" "$f" \
| head -n 1 \
| tr -d '\r' \
| sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'
return 0
fi
done
return 1
} }
subscription_add() { cmd_on() {
local name="$1" require_profiled
local url="$2" # shellcheck disable=SC1090
local headers="${3:-}" source "$PROFILED_FILE"
if [ -z "$name" ] || [ -z "$url" ]; then proxy_on
echo "[ERROR] 用法: clashctl sub add <name> <url> [headers]" >&2
exit 1
fi
ensure_subscription_file
if subscription_lookup "$name" >/dev/null; then
echo "[ERROR] 订阅已存在: $name" >&2
exit 1
fi
printf "%s|%s|%s|-\n" "$name" "$url" "$headers" >> "$SUBSCRIPTION_FILE"
echo "[OK] 已添加订阅: $name"
} }
subscription_del() { cmd_off() {
local name="$1" require_profiled
if [ -z "$name" ]; then # shellcheck disable=SC1090
echo "[ERROR] 用法: clashctl sub del <name>" >&2 source "$PROFILED_FILE"
exit 1 proxy_off
fi
ensure_subscription_file
if ! subscription_lookup "$name" >/dev/null; then
echo "[ERROR] 未找到订阅: $name" >&2
exit 1
fi
awk -F'|' -v target="$name" 'BEGIN{OFS=FS} $1 != target {print}' "$SUBSCRIPTION_FILE" > "${SUBSCRIPTION_FILE}.tmp"
mv "${SUBSCRIPTION_FILE}.tmp" "$SUBSCRIPTION_FILE"
echo "[OK] 已删除订阅: $name"
} }
subscription_use() { cmd_status() {
local name="$1" if service_exists; then
if [ -z "$name" ]; then systemctl --no-pager --full status "$Service_Name" || true
echo "[ERROR] 用法: clashctl sub use <name>" >&2 else
exit 1 warn "未检测到 systemd 服务,尝试检查进程"
fi ps -ef | grep -E 'clash|mihomo' | grep -v grep || true
ensure_subscription_file fi
local line
line=$(subscription_lookup "$name")
if [ -z "$line" ]; then
echo "[ERROR] 未找到订阅: $name" >&2
exit 1
fi
local url headers
url=$(echo "$line" | awk -F'|' '{print $2}')
headers=$(echo "$line" | awk -F'|' '{print $3}')
set_env_var "CLASH_URL" "$url"
set_env_var "CLASH_HEADERS" "$headers"
set_env_var "CLASH_SUBSCRIPTION" "$name"
echo "[OK] 已切换订阅: $name"
} }
subscription_touch() { cmd_proxy() {
local name="$1" require_profiled
local timestamp # shellcheck disable=SC1090
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") source "$PROFILED_FILE"
awk -F'|' -v target="$name" -v ts="$timestamp" 'BEGIN{OFS=FS} {if ($1==target) {$4=ts} print}' "$SUBSCRIPTION_FILE" > "${SUBSCRIPTION_FILE}.tmp"
mv "${SUBSCRIPTION_FILE}.tmp" "$SUBSCRIPTION_FILE" local sub="${1:-status}"
case "$sub" in
on) proxy_on ;;
off) proxy_off ;;
status) proxy_status ;;
*)
err "未知 proxy 子命令: $sub"
echo "用法: clashctl proxy [on|off|status]"
exit 1
;;
esac
} }
subscription_update() { cmd_ui() {
local name="${1:-}" local controller host port
if [ -z "$name" ]; then controller="$(read_config_value "external-controller" || true)"
name=$(awk -F= '/^export CLASH_SUBSCRIPTION=/{print $2}' "$ENV_FILE" | tr -d "'" | tr -d '"') [ -n "${controller:-}" ] || controller="127.0.0.1:9090"
fi
if [ -z "$name" ]; then host="${controller%:*}"
echo "[ERROR] 未指定订阅名称,且 CLASH_SUBSCRIPTION 未设置" >&2 port="${controller##*:}"
exit 1
fi case "$host" in
subscription_use "$name" 0.0.0.0|::|localhost)
run_script update.sh host="$(hostname -I 2>/dev/null | awk '{print $1}')"
subscription_touch "$name" [ -n "$host" ] || host="127.0.0.1"
echo "[OK] 订阅已更新: $name" ;;
esac
printf 'http://%s:%s/ui\n' "$host" "$port"
} }
subscription_list() { cmd_secret() {
ensure_subscription_file local secret
local active secret="$(read_config_value "secret" || true)"
active=$(awk -F= '/^export CLASH_SUBSCRIPTION=/{print $2}' "$ENV_FILE" | tr -d "'" | tr -d '"') if [ -n "${secret:-}" ]; then
printf "%-20s %-6s %s\n" "NAME" "ACTIVE" "URL" echo "$secret"
while IFS='|' read -r name url headers updated; do else
[ -z "$name" ] && continue err "未读取到 secret"
if [ "$name" = "$active" ]; then exit 1
printf "%-20s %-6s %s\n" "$name" "yes" "$url" fi
else
printf "%-20s %-6s %s\n" "$name" "no" "$url"
fi
done < "$SUBSCRIPTION_FILE"
} }
subscription_log() { cmd_sub() {
ensure_subscription_file local subcmd="${1:-show}"
printf "%-20s %s\n" "NAME" "LAST_UPDATE" case "$subcmd" in
while IFS='|' read -r name url headers updated; do show)
[ -z "$name" ] && continue if [ -f "$ENV_FILE" ]; then
printf "%-20s %s\n" "$name" "${updated:--}" grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || true
done < "$SUBSCRIPTION_FILE" else
err "未找到 .env"
exit 1
fi
;;
update)
if service_exists; then
info "重启服务以刷新订阅..."
systemctl restart "$Service_Name"
ok "订阅刷新完成"
elif [ -f "$Server_Dir/restart.sh" ]; then
bash "$Server_Dir/restart.sh"
ok "订阅刷新完成"
else
err "未找到 restart.sh"
exit 1
fi
;;
*)
err "未知 sub 子命令: $subcmd"
echo "用法: clashctl sub [show|update]"
exit 1
;;
esac
} }
status_fallback() { cmd_upgrade() {
if [ -f "$PID_FILE" ]; then if [ -f "$Server_Dir/update.sh" ]; then
local pid bash "$Server_Dir/update.sh"
pid=$(cat "$PID_FILE") else
if [ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1; then err "未找到 update.sh"
echo "[OK] Clash 进程运行中 (PID: $pid)" exit 1
return 0 fi
fi
fi
if pgrep -f "clash-linux-" >/dev/null 2>&1; then
echo "[OK] Clash 进程运行中"
return 0
fi
echo "[WARN] 未检测到 Clash 进程"
return 1
} }
run_script() { write_env_bool() {
local script="$1" local key="$1"
if [ ! -x "$CLASH_HOME/$script" ]; then local value="$2"
echo "[ERROR] 未找到脚本: $CLASH_HOME/$script" >&2
exit 1 if [ ! -f "$ENV_FILE" ]; then
fi err "未找到 .env"
( cd "$CLASH_HOME" && bash "$CLASH_HOME/$script" ) exit 1
fi
if grep -qE "^[[:space:]]*(export[[:space:]]+)?${key}=" "$ENV_FILE"; then
sed -i -E "s|^[[:space:]]*(export[[:space:]]+)?${key}=.*$|export ${key}=\"${value}\"|g" "$ENV_FILE"
else
echo "export ${key}=\"${value}\"" >> "$ENV_FILE"
fi
} }
command=${1:-} cmd_tun() {
case "$command" in local subcmd="${1:-status}"
start) case "$subcmd" in
action_with_systemd start || run_script start.sh status)
;; if [ -f "$ENV_FILE" ]; then
stop) grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置'
action_with_systemd stop || run_script shutdown.sh else
;; err "未找到 .env"
restart) exit 1
action_with_systemd restart || run_script restart.sh fi
;; ;;
status) on)
if use_systemd; then write_env_bool "CLASH_TUN" "true"
if systemctl status "${SERVICE_NAME}.service" --no-pager; then ok "已写入 CLASH_TUN=true"
exit 0 ;;
fi off)
fi write_env_bool "CLASH_TUN" "false"
status_fallback ok "已写入 CLASH_TUN=false"
;; ;;
update) *)
run_script update.sh err "未知 tun 子命令: $subcmd"
;; echo "用法: clashctl tun [on|off|status]"
set-url) exit 1
set_url "${2:-}" ;;
;; esac
sub) }
subcommand="${2:-}"
case "$subcommand" in cmd_mixin() {
add) local subcmd="${1:-status}"
subscription_add "${3:-}" "${4:-}" "${5:-}" local mixin_file="$Server_Dir/conf/mixin.yaml"
;;
del|delete|rm|remove) case "$subcmd" in
subscription_del "${3:-}" status)
;; if [ -f "$mixin_file" ]; then
use) ok "Mixin 已存在: $mixin_file"
subscription_use "${3:-}" else
;; warn "Mixin 不存在: $mixin_file"
update) fi
subscription_update "${3:-}" ;;
;; edit)
list|ls) ${EDITOR:-vi} "$mixin_file"
subscription_list ;;
;; *)
log) err "未知 mixin 子命令: $subcmd"
subscription_log echo "用法: clashctl mixin [status|edit]"
;; exit 1
*) ;;
echo "[ERROR] 未知订阅命令: $subcommand" >&2 esac
print_usage }
exit 1
;; main() {
esac local cmd="${1:-}"
;; shift || true
-h|--help|help|'')
print_usage case "$cmd" in
;; on) cmd_on "$@" ;;
*) off) cmd_off "$@" ;;
echo "[ERROR] 未知命令: $command" >&2 status) cmd_status "$@" ;;
print_usage proxy) cmd_proxy "$@" ;;
exit 1 ui) cmd_ui "$@" ;;
;; secret) cmd_secret "$@" ;;
esac sub) cmd_sub "$@" ;;
upgrade) cmd_upgrade "$@" ;;
tun) cmd_tun "$@" ;;
mixin) cmd_mixin "$@" ;;
-h|--help|"") usage ;;
*)
err "未知命令: $cmd"
echo
usage
exit 1
;;
esac
}
main "$@"