From 4c94d430b155e78609cd93e5bdbd796b301a4c7c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 08:40:46 +0800 Subject: [PATCH] =?UTF-8?q?=E2=98=85=E2=98=85=E2=98=85=20=E6=94=B6=20pi=20?= =?UTF-8?q?`9bb3cc32`:=20=E4=B8=8A=E4=B8=80=E7=89=88"=E8=AF=8D=E9=A6=96?= =?UTF-8?q?=E8=A7=84=E5=88=99"=E7=9A=84**=E5=8D=95=E6=9D=A1=20sed=20?= =?UTF-8?q?=E8=BF=91=E4=BC=BC**=E6=9C=89=E4=B8=A4=E5=A4=84**=E6=96=B9?= =?UTF-8?q?=E5=90=91=E7=9B=B8=E5=8F=8D**=E7=9A=84=E6=AE=8B=E7=95=99?= =?UTF-8?q?=EF=BC=88=E2=91=A0=E5=81=87=E9=98=B4=20`.=20"$A=20#B/=E2=80=A6"?= =?UTF-8?q?`=20=E2=87=92=20rc=3D0=20=E4=B8=A2=E8=B0=83=E7=94=A8=E8=80=85?= =?UTF-8?q?=20=E2=91=A1=E5=81=87=E7=BA=A2=20`.=20/dev/null;#=20=E6=8F=90?= =?UTF-8?q?=E5=88=B0=20=E2=80=A6`=20=E2=87=92=20rc=3D1=20=E8=AF=AF?= =?UTF-8?q?=E5=88=A4=EF=BC=89=E2=87=92=20=E6=94=B9=E4=B8=BA**=E5=BC=95?= =?UTF-8?q?=E5=8F=B7=E6=84=9F=E7=9F=A5=E7=9A=84=E9=80=90=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E6=89=AB=E6=8F=8F**=EF=BC=88`=5Fstrip=5Fcomments=5Flex`?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=B9=B6=E5=8A=A0=E8=B0=83=E7=94=A8=E8=80=85?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E7=9A=84**=E6=AD=A3/=E5=8F=8D=E5=90=91?= =?UTF-8?q?=E5=AF=B9=E7=85=A7**?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ★★ (A) pi 报的两处残留我都复现(`2fe58ce` 上逐字一致): ① 假阴(未全闭): `. "$A #B/deploy/lib/env-defaults.sh"` + 裸赋值 ⇒ 我的规则把**引号内「空格+#」**当注释 ⇒ 截成 `. "$A` ⇒ 丢失调用者 ⇒ rc=**0**、调用者数仍 3 ② 假红(我没提): `. /dev/null;# 提到 env-defaults.sh` + 裸赋值 ⇒ `;` 后的 `#` **该**截而没截 ⇒ **非调用者**进集合 ⇒ rc=**1** 并报该文件裸赋值 对照: `;` 换成空白 ⇒ 正确截断 ⇒ 不算调用者 ⇒ rc=0 ✓ ★ 两处**方向相反**且**单条 sed 修不了**: 只加元字符边界 ⇒ 修②、对①无改善; 只追引号 ⇒ 修①、对②无改善 ⇒ 真规则要**跨字符状态**(在引号里吗)⇒ 必须写成自动机 ★ (B) 修法: 新增 `_strip_comments_lex`(awk 逐字符扫描)实现 bash 手册的真规则 —— **以 `#` 开头的"词"**(词边界 = 行首/空白/元字符),且 `#` 在**引号内**时不是词首; `'…'` 内除 `'` 全字面,`"…"` 内 `\` 可转义,`\#` 不是注释。 实测矩阵(本仓真实两行 + pi 两例 + 反例 4 例)全部符合预期 ★ 我先试了 pi 建议的"env-defaults.sh 出现在最后一个引号闭合之后" —— **实测会漏掉本仓 **全部** 真实调用者**(`. "$REPO/…/env-defaults.sh"` 里 env 在末引号**之前**)⇒ 未采纳 ★ (C) 加调用者判定的**正向/反向对照**(域的定义静音 ⇒ 正式扫描不报错,只少查/多查文件): 正: 两个已知调用者样本(含本仓真实形态、pi①)必须被认出 反: 两个非调用者样本(pi②、整行注释)必须不被认出 ⇒ 变异测试: 词法器"恒删 env-defaults" ⇒ 正对照打红(但**先**被空集守卫接住,见下) 词法器"恒不截断" ⇒ **反对照打红**(这一条是新的鉴别力) ★ (D) ⚠️ 我**自catch 并把射程写窄**(第一版注释我写宽了): 该对照调的是**函数** `_is_caller_text`,**不是调用点** ⇒ 它盖住"词法器 + 正则"两环,**盖不住**有人把**调用点**改掉(`… "$(cat "$f")"` → `true`/`false`)—— 实测确认那种改法自检**照样绿**。⇒ 该方向由 ② 空集 / ②′ 下界兜; 而域**偏宽**(非调用者算进来 ⇒ 假红)**当前没有守卫**,我**不**声称已盖住。 ★ 这正是我先前记的"自检必须与正式判定走**同一条实现路径**"——我**又**在注释里写宽了一次; 这次的处理是**把射程写窄**而不是再宣称补了一个洞(按 ⑰′ 的标准) ★ 回归: pi 两例 + 前版两例 + 真调用者 共 5 例全部符合预期; 12 向变异矩阵全 rc=1;干净基线 rc=**0**;criteria-hygiene **10/10** 绿 ★ 依赖: `awk` 已在脚本顶部预检(`for c in grep awk sed find`)与 SELF 列表内 --- deploy/check-require-declaration.sh | 129 ++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/deploy/check-require-declaration.sh b/deploy/check-require-declaration.sh index 30e5a13..934c879 100755 --- a/deploy/check-require-declaration.sh +++ b/deploy/check-require-declaration.sh @@ -86,6 +86,78 @@ _scan_stripped() { grep -nE "$AM_SCAN_RE" <<< "$1"; } # 文本 ⇒ 违规行号(= 去注释 + 匹配),供**正向对照**用 _scan_text() { _scan_stripped "$(strip_text "$1")"; } +# ★★★ 调用者判定专用的去注释:**引号感知**的逐字符扫描(不是 sed 近似)。 +# pi `9bb3cc32` 实测出上一版"词首规则"`s/\(^\|[[:space:]]\)#.*$//` 有**两处方向相反**的残留: +# ① **假阴**(未全闭): `. "$A #B/deploy/lib/env-defaults.sh"` —— 引号内「空格+`#`」 +# 被误当注释 ⇒ 截成 `. "$A` ⇒ 又丢了调用者身份 ⇒ rc=0、调用者数仍 3 +# ② **假红**: `. /dev/null;# 提到 env-defaults.sh` —— `;` 后的 `#` **该**截而没截 +# ⇒ 一个**不是**调用者的文件进了集合 ⇒ rc=1 并报它的裸赋值 +# ★ 两处**方向相反**,且**单一正则/单个 sed 表达式修不了**: +# 只加元字符边界 ⇒ 修好 ②、对 ① 无改善;只追引号 ⇒ 修好 ①、对 ② 无改善。 +# ⇒ 真规则需要**跨字符状态**(在引号里吗)⇒ 必须写成**自动机**,不能写成一次替换。 +# ⇒ 本节实现 bash 手册的真规则: **以 `#` 开头的词**(词边界 = 行首 / 空白 / 元字符), +# 且 `#` 在**引号内**时不是词首(引号内容不被分词)。 +# · `'…'` 内除 `'` 外全部字面;`"…"` 内 `\` 可转义、`"` 结束 +# · `\#` 不是注释 +# 实测(原型矩阵,本仓真实两行 + pi 两例 + 反例): +# `. "$REPO/deploy/lib/env-defaults.sh"` ⇒ 提到 ✓(算调用者) +# `. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/…"` ⇒ 提到 ✓ +# `. "${REPO#/home}/deploy/lib/env-defaults.sh"` ⇒ 提到 ✓(上一版假绿) +# `. "$A #B/deploy/lib/env-defaults.sh"` ⇒ 提到 ✓(pi ①,已闭) +# `. /dev/null;# 提到 env-defaults.sh` ⇒ 截断 ⇒ 不算 ✓(pi ②,已闭) +# `. /dev/null # 提到 env-defaults.sh` ⇒ 截断 ⇒ 不算 ✓ +# `# . env-defaults.sh`(整行注释) ⇒ 空 ⇒ 不算 ✓ +# `X="a#b"` ⇒ 不截断 ✓ +# ⚠️ 射程: 这是**行级近似**的真 shell 规则(不处理 `$( … )` 里跨行的引号、heredoc、 +# 续行拼接)—— 足以判"这一行是不是 source 行",**不**声称是通用 shell 词法器。 +# ★ 与 ⑧b 的关系: 本节闭的是**调用者判定**(⑧c);正式违规扫描仍走 `strip_text`, +# ⑧b(注释语义)那一半**未**闭,本处不声称。 +_strip_comments_lex() { + awk ' + { + line = $0; n = length(line); out = ""; prev = 1; sq = 0; dq = 0; esc = 0 + for (i = 1; i <= n; i++) { + c = substr(line, i, 1) + if (esc) { out = out c; esc = 0; prev = 0; continue } + if (sq) { out = out c; if (c == "\x27") sq = 0; continue } + if (dq) { + if (c == "\\") { out = out c; esc = 1; continue } + out = out c; if (c == "\"") dq = 0; continue + } + if (c == "\\") { out = out c; esc = 1; prev = 0; continue } + if (c == "\x27") { out = out c; sq = 1; prev = 0; continue } + if (c == "\"") { out = out c; dq = 1; prev = 0; continue } + if (c == "#") { + # 词首 = 行首,或前一个字符是空白/元字符 + if (prev) break # 注释开始 ⇒ 本行到此为止 + out = out c; prev = 0; continue + } + out = out c + if (c == " " || c == "\t" || c == ";" || c == "|" || c == "&" || \ + c == "(" || c == ")" || c == "<" || c == ">") prev = 1 + else prev = 0 + } + print out + }' "$1" +} + +# ★★★ "这一行/这段文本算不算 source 了本库" 的**唯一**判定实现。 +# ★ 为什么必须是**唯一**的: 正式扫描与自检**都**经由它 —— +# 否则把调用点换掉(例如退回 `cat`)时自检**照样绿**,而域已经错了。 +# 我第一版就是"自检直接调 `_strip_comments_lex`",实测**共模** +# (调用点被换成 `cat` 时自检不响)⇒ 抽成这一个函数,调用点也是它。 +# 判法: 去注释(词法器)后,**行首**是 `.` 或 `source` 且**提到** env-defaults.sh。 +_is_caller_text() { + local t + t="$(printf '%s\n' "$1" | _strip_comments_lex /dev/stdin)" + case "$t" in + *env-defaults.sh*) + grep -qE '^[[:space:]]*(\.|source)[[:space:]].*env-defaults\.sh' <<< "$t" + ;; + *) return 1 ;; + esac +} + # ① 找"调用者":非注释行里以 `. ` 或 `source ` 起头、且提到 env-defaults.sh 的文件。 callers="" for f in $(find deploy -name '*.sh' -type f | sort); do @@ -106,26 +178,26 @@ for f in $(find deploy -name '*.sh' -type f | sort); do # `. other.sh # 注释里提到 env-defaults.sh` # 当成调用者 ⇒ **假红**(安全方向,但仍是错的)。 # 也不能用 `strip_text`(它是 `s/#.*$//`,正是被 pi 抓到的那个错)。 - # ⇒ 用 `_caller_strip`: 只在 `#` 位于**行首或空白之后**时才截断 ⇒ - # `${REPO#/home}` / `${BASH_SOURCE[0]#/x}` / `X="a#b"` **都不被截断** ✓ - # 而 `foo.sh # 提到 env-defaults.sh` **被截断**(不误判,无假红)✓ - # 整行注释 `# . env-defaults.sh` ⇒ 截成空 ⇒ 不匹配 ✓ - # ★ 这不是"换一个实现",是**把注释的规格写对**: 原 `s/#.*$//` 把 - # "参数展开/引号内的 `#`"也当成了注释 —— 那是**规格错**,不是"边界"。 + # ★★ 第二版(pi `9bb3cc32` 报回**两处方向相反**的残留后重做): + # 上一版是"词首规则"的**单条 sed 近似**,它同时缺**元字符边界**与**引号状态** + # ⇒ 一处假阴、一处假红(两例见 `_strip_comments_lex` 上方的实测记录)。 + # ⇒ 现改为调用 `_strip_comments_lex`(**引号感知的逐字符扫描**)—— + # 理由: 真规则要"当前是否在引号内"这个**跨字符状态**,一次 `s///` 表达不了。 # ⚠️ 射程(本处**只**修调用者判定这一半): 正式违规扫描仍走 `strip_text` # (`s/#.*$//`)⇒ 若某**违规行**本身含 `${VAR#…}` 且截断后不再匹配 `AM_SCAN_RE`, # 那仍会漏(属 ⑧b 的**语义**半,真边界)—— 本处**不**声称那半已闭。 - _ftext="$(sed 's/\(^\|[[:space:]]\)#.*$//' "$f")" - case "$_ftext" in - *env-defaults.sh*) - # 无管道匹配(`<<<` here-string):不会因 SIGPIPE 造出不稳定的 rc。 - if grep -qE '^[[:space:]]*(\.|source)[[:space:]].*env-defaults\.sh' <<< "$_ftext"; then - callers="$callers $f" - fi - ;; - esac + # ⇒ 调用点只用 `_is_caller_text`(它内部走词法器)。 + # ⚠️⚠️ **射程(我实测后收窄 —— 第一版我在这里写宽了)**: + # 本对照调的是**函数** `_is_caller_text`,**不是调用点**。 + # 所以它盖住的是: 词法器(引号状态/元字符边界)+ 那条正则。 + # 它**盖不住**: 有人把**调用点**改掉(`_is_caller_text "$(cat "$f")"` → `true`/`false`)—— + # 那种改法**不改函数**,自检照样绿(我实测确认)。 + # ⇒ 该方向由别的层兜: 域**空**由 ② 空集守卫、域**偏窄**由 ②′ 下界守卫; + # 而域**偏宽**(把非调用者算进来 ⇒ 假红)**当前没有守卫** —— 我**不**声称它已被盖住。 + if _is_caller_text "$(cat "$f")"; then + callers="$callers $f" + fi done -unset _ftext # shellcheck disable=SC2086 set -- $callers n_callers=$# @@ -211,6 +283,31 @@ AGENTMAIL_REQUIRE="git go"' || true)" esac unset _pc + # ★★★ 调用者判定的**正向对照**(2026-09-25,pi `9bb3cc32` 之后加): + # 为什么必须加: 调用者判定在**每条命令**开头就跑,是**域的定义**—— + # 若它静音(认不出任何人)或过宽(把非调用者认成调用者), + # **正式扫描不会报错**,只会"少查/多查文件" ⇒ 与"共模失效"同型。 + # ★★ 第一版我写成 "直接调 `_strip_comments_lex`",**实测在维度上更窄**(我自己抓到): + # 它只对照"词法器"这一环,**不**对照 `_is_caller_text` 里那条正则; + # ⇒ 抽成 `_is_caller_text` 后,对照覆盖"词法器 + 正则"**两环**(仍未覆盖调用点,见 ① 处射程)。 + # ★ 走同一函数才能对照到"引号状态 + 元字符边界"这两处(而不是只对照一个正则)。 + _pc_ok=0; _is_caller_text '. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/env-defaults.sh"' && _pc_ok=1 + _pc_ok2=0; _is_caller_text '. "$A #B/deploy/lib/env-defaults.sh"' && _pc_ok2=1 + if [ "$_pc_ok" -ne 1 ] || [ "$_pc_ok2" -ne 1 ]; then + printf ' [FAIL] 判据自检失败(调用者判定):**已知调用者样本**没被认出 ——\n' >&2 + printf ' ⇒ 域会**静默偏小**,域外的违规整批不查(防空转守卫只在"空集"时响)。按红计。\n' >&2 + exit 1 + fi + _pc_bad="" + _is_caller_text '. /dev/null;# 提到 env-defaults.sh' && _pc_bad="分号后的注释(该截没截)" + _is_caller_text '# . env-defaults.sh' && _pc_bad="${_pc_bad}整行注释" + if [ -n "$_pc_bad" ]; then + printf ' [FAIL] 判据自检失败(调用者判定,反向):**非调用者样本**被判成了调用者(%s)——\n' "$_pc_bad" >&2 + printf ' ⇒ 域会**偏大**,与该库无关的文件被拖进来报红(假红)。按红计。\n' >&2 + exit 1 + fi + unset _pc_ok _pc_ok2 _pc_bad + # ★ 反向对照(2026-09-25 我补,pi 的 §五 让我去看这一维) # ⚠️ **我实测后降级了它的说法 —— 它不改门禁判定,只改"报的是哪一句"**: # 把 `AM_SCAN_RE` 放宽到能匹配**合规**行(例如 `'='`)时: