Update clashctl

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

536
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() {
if [ ! -f "$PROFILED_FILE" ]; then
err "未安装 Shell 代理快捷命令:$PROFILED_FILE"
exit 1
fi
}
has_systemd() {
command -v systemctl >/dev/null 2>&1
}
service_exists() {
has_systemd && systemctl list-unit-files 2>/dev/null | grep -q "^${Service_Name}"
}
read_config_value() {
local key="$1" local key="$1"
local value="${2:-}" local f
if [ ! -f "$ENV_FILE" ]; then for f in "$TEMP_CONF_FILE" "$CONF_FILE"; do
echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2 if [ -f "$f" ]; then
exit 1 sed -nE "s/^[[:space:]]*${key}:[[:space:]]*//p" "$f" \
fi | head -n 1 \
local escaped escaped_sed | tr -d '\r' \
escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g") | sed -E 's/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/'
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() {
local url="$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() {
mkdir -p "$(dirname "$SUBSCRIPTION_FILE")"
if [ ! -f "$SUBSCRIPTION_FILE" ]; then
touch "$SUBSCRIPTION_FILE"
fi
}
subscription_lookup() {
local name="$1"
awk -F'|' -v target="$name" '$1 == target {print; exit}' "$SUBSCRIPTION_FILE"
}
subscription_add() {
local name="$1"
local url="$2"
local headers="${3:-}"
if [ -z "$name" ] || [ -z "$url" ]; then
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() {
local name="$1"
if [ -z "$name" ]; then
echo "[ERROR] 用法: clashctl sub del <name>" >&2
exit 1
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() {
local name="$1"
if [ -z "$name" ]; then
echo "[ERROR] 用法: clashctl sub use <name>" >&2
exit 1
fi
ensure_subscription_file
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() {
local name="$1"
local timestamp
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
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"
}
subscription_update() {
local name="${1:-}"
if [ -z "$name" ]; then
name=$(awk -F= '/^export CLASH_SUBSCRIPTION=/{print $2}' "$ENV_FILE" | tr -d "'" | tr -d '"')
fi
if [ -z "$name" ]; then
echo "[ERROR] 未指定订阅名称,且 CLASH_SUBSCRIPTION 未设置" >&2
exit 1
fi
subscription_use "$name"
run_script update.sh
subscription_touch "$name"
echo "[OK] 订阅已更新: $name"
}
subscription_list() {
ensure_subscription_file
local active
active=$(awk -F= '/^export CLASH_SUBSCRIPTION=/{print $2}' "$ENV_FILE" | tr -d "'" | tr -d '"')
printf "%-20s %-6s %s\n" "NAME" "ACTIVE" "URL"
while IFS='|' read -r name url headers updated; do
[ -z "$name" ] && continue
if [ "$name" = "$active" ]; then
printf "%-20s %-6s %s\n" "$name" "yes" "$url"
else
printf "%-20s %-6s %s\n" "$name" "no" "$url"
fi
done < "$SUBSCRIPTION_FILE"
}
subscription_log() {
ensure_subscription_file
printf "%-20s %s\n" "NAME" "LAST_UPDATE"
while IFS='|' read -r name url headers updated; do
[ -z "$name" ] && continue
printf "%-20s %s\n" "$name" "${updated:--}"
done < "$SUBSCRIPTION_FILE"
}
status_fallback() {
if [ -f "$PID_FILE" ]; then
local pid
pid=$(cat "$PID_FILE")
if [ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1; then
echo "[OK] Clash 进程运行中 (PID: $pid)"
return 0 return 0
fi fi
fi done
if pgrep -f "clash-linux-" >/dev/null 2>&1; then
echo "[OK] Clash 进程运行中"
return 0
fi
echo "[WARN] 未检测到 Clash 进程"
return 1 return 1
} }
run_script() { cmd_on() {
local script="$1" require_profiled
if [ ! -x "$CLASH_HOME/$script" ]; then # shellcheck disable=SC1090
echo "[ERROR] 未找到脚本: $CLASH_HOME/$script" >&2 source "$PROFILED_FILE"
exit 1 proxy_on
fi
( cd "$CLASH_HOME" && bash "$CLASH_HOME/$script" )
} }
command=${1:-} cmd_off() {
case "$command" in require_profiled
start) # shellcheck disable=SC1090
action_with_systemd start || run_script start.sh source "$PROFILED_FILE"
;; proxy_off
stop) }
action_with_systemd stop || run_script shutdown.sh
;; cmd_status() {
restart) if service_exists; then
action_with_systemd restart || run_script restart.sh systemctl --no-pager --full status "$Service_Name" || true
;; else
status) warn "未检测到 systemd 服务,尝试检查进程"
if use_systemd; then ps -ef | grep -E 'clash|mihomo' | grep -v grep || true
if systemctl status "${SERVICE_NAME}.service" --no-pager; then
exit 0
fi fi
fi }
status_fallback
;; cmd_proxy() {
update) require_profiled
run_script update.sh # shellcheck disable=SC1090
;; source "$PROFILED_FILE"
set-url)
set_url "${2:-}" local sub="${1:-status}"
;; case "$sub" in
sub) on) proxy_on ;;
subcommand="${2:-}" off) proxy_off ;;
case "$subcommand" in status) proxy_status ;;
add)
subscription_add "${3:-}" "${4:-}" "${5:-}"
;;
del|delete|rm|remove)
subscription_del "${3:-}"
;;
use)
subscription_use "${3:-}"
;;
update)
subscription_update "${3:-}"
;;
list|ls)
subscription_list
;;
log)
subscription_log
;;
*) *)
echo "[ERROR] 未知订阅命令: $subcommand" >&2 err "未知 proxy 子命令: $sub"
print_usage echo "用法: clashctl proxy [on|off|status]"
exit 1 exit 1
;; ;;
esac esac
}
cmd_ui() {
local controller host port
controller="$(read_config_value "external-controller" || true)"
[ -n "${controller:-}" ] || controller="127.0.0.1:9090"
host="${controller%:*}"
port="${controller##*:}"
case "$host" in
0.0.0.0|::|localhost)
host="$(hostname -I 2>/dev/null | awk '{print $1}')"
[ -n "$host" ] || host="127.0.0.1"
;; ;;
-h|--help|help|'') esac
print_usage
printf 'http://%s:%s/ui\n' "$host" "$port"
}
cmd_secret() {
local secret
secret="$(read_config_value "secret" || true)"
if [ -n "${secret:-}" ]; then
echo "$secret"
else
err "未读取到 secret"
exit 1
fi
}
cmd_sub() {
local subcmd="${1:-show}"
case "$subcmd" in
show)
if [ -f "$ENV_FILE" ]; then
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_URL=' "$ENV_FILE" || true
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
;; ;;
*) *)
echo "[ERROR] 未知命令: $command" >&2 err "未知 sub 子命令: $subcmd"
print_usage echo "用法: clashctl sub [show|update]"
exit 1 exit 1
;; ;;
esac esac
}
cmd_upgrade() {
if [ -f "$Server_Dir/update.sh" ]; then
bash "$Server_Dir/update.sh"
else
err "未找到 update.sh"
exit 1
fi
}
write_env_bool() {
local key="$1"
local value="$2"
if [ ! -f "$ENV_FILE" ]; then
err "未找到 .env"
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
}
cmd_tun() {
local subcmd="${1:-status}"
case "$subcmd" in
status)
if [ -f "$ENV_FILE" ]; then
grep -E '^[[:space:]]*(export[[:space:]]+)?CLASH_TUN=' "$ENV_FILE" || echo 'CLASH_TUN=未配置'
else
err "未找到 .env"
exit 1
fi
;;
on)
write_env_bool "CLASH_TUN" "true"
ok "已写入 CLASH_TUN=true"
;;
off)
write_env_bool "CLASH_TUN" "false"
ok "已写入 CLASH_TUN=false"
;;
*)
err "未知 tun 子命令: $subcmd"
echo "用法: clashctl tun [on|off|status]"
exit 1
;;
esac
}
cmd_mixin() {
local subcmd="${1:-status}"
local mixin_file="$Server_Dir/conf/mixin.yaml"
case "$subcmd" in
status)
if [ -f "$mixin_file" ]; then
ok "Mixin 已存在: $mixin_file"
else
warn "Mixin 不存在: $mixin_file"
fi
;;
edit)
${EDITOR:-vi} "$mixin_file"
;;
*)
err "未知 mixin 子命令: $subcmd"
echo "用法: clashctl mixin [status|edit]"
exit 1
;;
esac
}
main() {
local cmd="${1:-}"
shift || true
case "$cmd" in
on) cmd_on "$@" ;;
off) cmd_off "$@" ;;
status) cmd_status "$@" ;;
proxy) cmd_proxy "$@" ;;
ui) cmd_ui "$@" ;;
secret) cmd_secret "$@" ;;
sub) cmd_sub "$@" ;;
upgrade) cmd_upgrade "$@" ;;
tun) cmd_tun "$@" ;;
mixin) cmd_mixin "$@" ;;
-h|--help|"") usage ;;
*)
err "未知命令: $cmd"
echo
usage
exit 1
;;
esac
}
main "$@"