#!/bin/bash # ============================================================ # OpenDance Studio Web API —— deploy/.env 语法预检 # # 本文件被 install.sh 与 ctl.sh 引用(不要单独执行)。 # # 为什么需要: # 两个脚本都用 `set -a; . deploy/.env` 载入配置,shell 会**真的执行** # 这个文件。若某个值含空格或 shell 元字符却没加引号,这一行会被切成 # 「赋值 + 命令」,报错形如: # ./deploy/.env: line 53: Miku.pmx: command not found # 退出码 127,而且提示里看不出是哪个文件、哪一行、该怎么改。 # 载入前先做一次静态检查,给出带行号与修复建议的中文提示。 # # 约定(与 init-env.sh 一致): # 普通值不加引号(如 PORT=3000);值里含空格或 shell 元字符时用引号包起来, # 例如 MODEL_CONVERT_PMX="Hatsune Miku.pmx"。双引号内不要用 $(...) 或反引号 # —— 它们在载入时会被真正执行。单引号是字面量,最安全。 # ============================================================ # 裸值(未加引号)是否可以被 `set -a; . file` 安全载入 # 赋值语句里的裸值不会做通配展开,所以 * ? [ ] { } ! % + , - . : / @ = 都安全; # 真正会破坏解析的是:空白、& | ; $ ` < > ( ) \ 以及引号本身、行首的 ~ opendance_env_value_is_safe_unquoted() { local v="${1:-}" [ -z "$v" ] && return 0 case "$v" in *[[:space:]]*|*'&'*|*'|'*|*';'*|*'$'*|*'`'*|*'<'*|*'>'*|*'('*|*')'*|*'\'*|*'"'*|*"'"*) return 1 ;; '~'*) return 1 ;; esac return 0 } # 值本身是否是安全的写法(空值 / 加引号 / 裸值但字符安全) # 允许「引号 + 行尾注释」的写法,例如 DB_HOST="127.0.0.1" # 本机 opendance_env_value_is_safe() { local v="${1:-}" inner="" rest="" case "$v" in '') return 0 ;; \"*) case "$v" in *\"*) ;; *) return 1 ;; esac # 结束引号必须在 inner="${v#\"}"; inner="${inner%%\"*}" # 引号内的内容 case "$inner" in *'$('*|*'`'*) return 1 ;; esac # 双引号内会真的执行 rest="${v#\"}"; rest="${rest#*\"}" # 结束引号之后的内容 case "$rest" in ''|[[:space:]]|[[:space:]]*|[[:space:]]*'#'*) return 0 ;; *) return 1 ;; esac ;; \'*) case "$v" in *\'*) ;; *) return 1 ;; esac rest="${v#\'}"; rest="${rest#*\'}" case "$rest" in ''|[[:space:]]|[[:space:]]*|[[:space:]]*'#'*) return 0 ;; *) return 1 ;; esac ;; *) opendance_env_value_is_safe_unquoted "$v" ;; esac } # 校验一个 .env 文件:opendance_validate_env_file <文件路径> # 通过返回 0;发现不安全写法返回 1(问题行与其改法已打印到 stderr) opendance_validate_env_file() { local file="${1:-}" lineno=0 bad=0 line key value reason inner if [ ! -f "$file" ]; then printf '[env] 找不到配置文件:%s\n' "$file" >&2 return 1 fi while IFS= read -r line || [ -n "$line" ]; do lineno=$((lineno + 1)) line="${line%$'\r'}" # 兼容 CRLF case "$line" in ''|'#'*) continue ;; esac reason="" case "$line" in [A-Za-z_]*=*) ;; *) reason="不是「键=值」形式(载入时会被当成命令执行)" ;; esac if [ -z "$reason" ]; then key="${line%%=*}" value="${line#*=}" case "$key" in *[!A-Za-z0-9_]*) reason="键名含非法字符" ;; esac fi if [ -z "$reason" ] && ! opendance_env_value_is_safe "$value"; then case "$value" in \"*) inner="${value#\"}" case "$inner" in *\"*) reason="引号写法不安全:引号后有其它内容,或引号内用了 \$( ) / 反引号(载入时会真的执行)" ;; *) reason="双引号未闭合" ;; esac ;; \'*) inner="${value#\'}" case "$inner" in *\'*) reason="单引号后还有其它内容,请把整个值放进引号" ;; *) reason="单引号未闭合" ;; esac ;; *) reason="值含空格或 shell 元字符,却未加引号" ;; esac fi if [ -n "$reason" ]; then bad=$((bad + 1)) printf '\033[1;31m[env]\033[0m %s 第 %d 行写法不安全:%s\n' "$file" "$lineno" "$reason" >&2 printf ' %d | %s\n' "$lineno" "$line" >&2 case "$line" in *'='*) key="${line%%=*}"; value="${line#*=}" case "$value" in \"*|\'*) printf ' 提示: 引号请成对、且把整个值包住; 单引号是字面量, 最安全。\n' >&2 ;; *"'"*) printf ' 提示: 值里同时有单引号与特殊字符, 请手工调整 - 单引号内不能再出现单引号。\n' >&2 ;; *'$'*|*'`'*) printf " 改法: %s='%s' 注意: 双引号里的 \$( ) 与反引号会被真的执行, 单引号才是字面量。\n" "$key" "$value" >&2 ;; *) printf ' 改法: %s="%s"\n' "$key" "$value" >&2 ;; esac ;; esac fi done < "$file" if [ "$bad" -gt 0 ]; then printf '\033[1;31m[env]\033[0m %s 语法预检未通过:%d 行需要加引号(见上)。\n' "$file" "$bad" >&2 return 1 fi return 0 }