#!/bin/bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) SERVICE_NAME="clash-for-linux" resolve_clash_home() { local candidates if [ -n "${CLASH_HOME:-}" ] && [ -d "$CLASH_HOME" ]; then echo "$CLASH_HOME" return 0 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) ENV_FILE="$CLASH_HOME/.env" PID_FILE="$CLASH_HOME/temp/clash.pid" SUBSCRIPTION_FILE="$CLASH_HOME/conf/subscriptions.list" use_systemd() { command -v systemctl >/dev/null 2>&1 } 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 < [args] Commands: start Start Clash service stop Stop Clash service restart Restart Clash service status Show Clash service status update Refresh subscription config set-url Update CLASH_URL in .env sub add [headers] Add subscription entry sub del Delete subscription entry sub use Activate subscription entry sub update [name] Update subscription config by entry sub list List subscriptions sub log Show subscription update logs Environment: CLASH_HOME Override Clash installation directory USAGE } set_env_var() { local key="$1" local value="${2:-}" if [ ! -f "$ENV_FILE" ]; then echo "[ERROR] 未找到 .env 文件: $ENV_FILE" >&2 exit 1 fi local escaped escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g") if grep -q "^export ${key}=" "$ENV_FILE"; then sed -i "s@^export ${key}=.*@export ${key}='${escaped}'@" "$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 [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 " >&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 " >&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 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() { local script="$1" if [ ! -x "$CLASH_HOME/$script" ]; then echo "[ERROR] 未找到脚本: $CLASH_HOME/$script" >&2 exit 1 fi ( cd "$CLASH_HOME" && bash "$CLASH_HOME/$script" ) } command=${1:-} case "$command" in start) action_with_systemd start || run_script start.sh ;; stop) action_with_systemd stop || run_script shutdown.sh ;; restart) action_with_systemd restart || run_script restart.sh ;; status) if use_systemd; then if systemctl status "${SERVICE_NAME}.service" --no-pager; then exit 0 fi fi status_fallback ;; update) run_script update.sh ;; set-url) set_url "${2:-}" ;; sub) subcommand="${2:-}" case "$subcommand" in 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 print_usage exit 1 ;; esac ;; -h|--help|help|'') print_usage ;; *) echo "[ERROR] 未知命令: $command" >&2 print_usage exit 1 ;; esac