fix(deploy): .env 中带空格的值未加引号导致 install.sh 以 127 失败

现象:
  ./deploy/.env: line 53: Miku.pmx: command not found
  [install] 第 53 行执行失败(退出码 127)

根因:deploy/.env.example 的 MODEL_CONVERT_PMX=Hatsune Miku.pmx 值含空格
却没加引号,install.sh 用 `set -a; . deploy/.env` 载入时被切成
「赋值 + 命令」,Miku.pmx 被当成命令执行。

修复:
1. 模板改为 MODEL_CONVERT_PMX="Hatsune Miku.pmx"(占位符值同样加引号);
2. 新增 deploy/env-lib.sh:.env 语法预检,install.sh / ctl.sh 在 source
   之前检查,报错给出「哪一行 / 原文 / 改法」, 与反引号建议单引号;
3. install.sh 的 ERR trap 补上出错文件名(lineno 属于被 source 的 .env,
   只报行号无法定位);
4. 新增 test/env-file.spec.ts 12 项回归测试,锁住「模板可被 shell 原样载入」;
5. 版本升到 0.2.1,README(.md/.txt)/DSH.md 补 .env 写法约定、FAQ 与测试数量。
This commit is contained in:
opendance
2026-09-15 22:26:58 +08:00
parent d6376ec5a5
commit ea58520fc2
14 changed files with 412 additions and 33 deletions
+5 -3
View File
@@ -24,10 +24,10 @@ HOST=0.0.0.0
ADMIN_DIR=/opt/opendance/admin
# ---- 现有 MySQL / MariaDB 实例(本项目不安装、不管理数据库)----
DB_HOST=<existing-mysql-host>
DB_HOST='<existing-mysql-host>'
DB_PORT=3306
DB_USER=opendance_app
DB_PASS=<数据库口令>
DB_PASS='<数据库口令>'
DB_NAME=opendance
# 生产环境必须为 false,表结构一律走 TypeORM migration
DB_SYNCHRONIZE=false
@@ -50,7 +50,9 @@ ASSIMP_BIN=assimp
# humanoid 骨骼名映射不全时会自动回退交付 GLB 并写进 conversion_note
MODEL_CONVERT_FORMAT=glb
# 压缩包里优先选用的 PMX 文件名(大小写不敏感);找不到时按"格式优先级 -> 目录最浅 -> 文件最大"挑
MODEL_CONVERT_PMX=Hatsune Miku.pmx
# 值里含空格或 shell 元字符时必须用引号包起来(install.sh 会用
# `set -a; . deploy/.env` 真的执行本文件;不加引号会把 "Miku.pmx" 当命令)
MODEL_CONVERT_PMX="Hatsune Miku.pmx"
# 单次转换超时(毫秒)
MODEL_CONVERT_TIMEOUT_MS=180000
+8 -1
View File
@@ -13,6 +13,7 @@
# ============================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
APP_DIR="${APP_DIR:-/opt/opendance}"
SERVICE_USER="${SERVICE_USER:-opendance}"
LOG_FILE="${LOG_FILE:-/var/log/opendance-api.log}"
@@ -95,8 +96,14 @@ start() {
return 0
fi
# 载入部署环境变量
# 载入部署环境变量(先做语法预检:.env 会被 shell 真的执行)
if [ -f "$APP_DIR/deploy/.env" ]; then
if [ -f "$SCRIPT_DIR/env-lib.sh" ]; then
# shellcheck source=deploy/env-lib.sh
. "$SCRIPT_DIR/env-lib.sh"
opendance_validate_env_file "$APP_DIR/deploy/.env" \
|| die "$APP_DIR/deploy/.env 语法预检未通过(见上):给对应行的值加上引号后重试。"
fi
set -a; . "$APP_DIR/deploy/.env"; set +a
else
die "缺少 $APP_DIR/deploy/.env"
+126
View File
@@ -0,0 +1,126 @@
#!/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
}
+11 -1
View File
@@ -21,11 +21,18 @@
# ============================================================
set -euo pipefail
# 失败时打印出错行号,避免只看到半截输出
trap 'printf "\033[1;31m[install]\033[0m 第 %d 行执行失败(退出码 %d)\n" "$LINENO" "$?" >&2' ERR
trap 'printf "\033[1;31m[install]\033[0m %s 第 %d 行执行失败(退出码 %d)\n" "${BASH_SOURCE[0]}" "$LINENO" "$?" >&2' ERR
SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$SRC_DIR"
# .env 语法预检(deploy/env-lib.sh):.env 是被 shell 真正 source 的文件,
# 未加引号且含空格的值会被当成命令执行,报错形如
# ./deploy/.env: line 53: Miku.pmx: command not found
# 载入前先查一遍,给出带行号与改法的中文提示。
# shellcheck source=deploy/env-lib.sh
. "$SRC_DIR/deploy/env-lib.sh"
APP_DIR="${APP_DIR:-/opt/opendance}"
SERVICE_USER="${SERVICE_USER:-opendance}"
STORAGE_DIR="${STORAGE_DIR:-/var/lib/opendance/storage}"
@@ -125,6 +132,8 @@ if [ ! -f deploy/.env ]; then
fi
fi
opendance_validate_env_file "./deploy/.env" || die "deploy/.env 语法预检未通过(见上):按提示给对应行的值加上引号后重新执行。"
set -a
# shellcheck disable=SC1091
. ./deploy/.env
@@ -320,6 +329,7 @@ fi
# ---------- 3. 数据库迁移 ----------
log "执行 TypeORM 迁移(不使用 DB_SYNCHRONIZE)"
opendance_validate_env_file "$APP_DIR/deploy/.env" || die "$APP_DIR/deploy/.env 语法预检未通过(见上)"
set -a; . "$APP_DIR/deploy/.env"; set +a
cd "$APP_DIR/web-server"
as_service_user env \