Files
clash-for-linux/clashctl

303 lines
7.1 KiB
Bash
Executable File

#!/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 <<USAGE
Usage: clashctl <command> [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 <url> Update CLASH_URL in .env
sub add <name> <url> [headers] Add subscription entry
sub del <name> Delete subscription entry
sub use <name> 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 <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
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