mirror of
https://github.com/wnlen/clash-for-linux.git
synced 2026-03-21 22:06:45 +08:00
Update clashctl
This commit is contained in:
520
clashctl
520
clashctl
@ -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 on,Tab 补全更方便!
|
||||||
|
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 "$@"
|
||||||
Reference in New Issue
Block a user