Web 平台 API / glob · 文件路径的迷你匹配语言 / 真实管线:parseGlob → AST → regex 源串 → 引擎执行 待审核 5 / 6
05 · 引擎

真实管线:parseGlob → AST → regex 源串 → 引擎执行

glob 不自带匹配算法。主流实现(JS 的 minimatch、Python 的 glob 模块)走同一条路:把 glob 翻译成一条正则,再交给正则引擎执行。本页直接拆开本仓库的真实实现 @vega/parsing/glob——前面 0104 页的全部 demo 都由它驱动:

图 1 · 从 pattern 到匹配结果的完整管线:分词、AST、转译成正则、执行。

四个阶段各居一个源文件:parser.ts(字符级递归下降 → AST)、ast.ts(节点类型)、translate.ts(AST → regex 源串)、matcher.ts(调 @vega/parsing/regexcompile 拿 matcher)。glob 这边不实现任何匹配算法——回溯、字符类、锚定全由 regex 引擎负责,这正是「编译到已有 IR」的取舍。本页假设已熟悉 0104 页的语法语义;regex 侧的语法与引擎见 regex 系列

1 · 实时拆解:AST 与转译产物

翻译规则不长:字面量做 regex 元字符转义;*[^/]*(段内任意,刻意排除 /)、?[^/][!a-z][^/a-z](取反类额外排除 /,防止越段); {a,b} → 分支 (?:a|b);独占整段的 ** 与相邻的 / 合并,生成 (?:seg/)* 一类「零或多段」结构;dot 规则就是在通配符起头的段前插一枚 否定先行断言 (?!\.);最后两头加 ^…$ 锚定(glob 是整体匹配,不是含子串)。解析失败时返回 Result 形态的 { ok:false, error:{ message, pos } }——可用预设里两个非法 pattern。

图 1-1 · 转译规则逐条对照,可改 pattern 观察产物正则如何变化。

glob 是「刻意收窄的正则子集」。 注意译文里通配全用 [^/]* 而非正则的 .*——/ 被刻意排除在外,这正是「不跨段」的来源。glob 没有反向引用,回溯面比通用正则窄——但这不等于免疫 灾难性回溯 (ReDoS)。相邻的两个 [^/]* 之间只隔一个字面字符就足以爆炸:*a*a*a*a*a*a*a*a*b 译出的正则喂 a 重复串,实测每加两个字符耗时翻倍——n=24 时 9.9ms、n=28 时 44ms、n=32 时 159ms(Chrome 与 Node 同源的 V8,核对于 2026-08)。minimatch 历史上的 ReDoS 通告正出自这一类 pattern。上面的 verdict 还把同一条源串喂给浏览器原生 RegExp 复核——产物就是一条普通正则。

2 · 另一条实现路线:段内 * 的双指针回溯

不经正则也能实现 glob:POSIX 的 fnmatch(3) 直接在 pattern 与字符串上匹配(注意 Python 标准库的 fnmatch 不是这一路:它转成 .* 形式的正则、*/、也不遵守 dot 规则,路径感知的活由 globpathlib 承担)。只有 *? 时,有个不用递归的经典算法——两个指针 + 一对寄存器:pp 扫 pattern、s 扫 str;遇到 *先假设它消费 0 个字符,并用 star / mark 记下「失配时回到此处,让它多消费一个」。下面单步看它怎么走——这也是上面 AST 那条 regex 里 [^/]* 在引擎内部回溯的微观图景。

图 2-1 · 双指针匹配的单步执行,可观察遇到星号时的记忆点与失配后的回退。

这条双指针算法本身没有指数爆炸。 mark 单调递增、s 重置后只会更靠后,所以总步数有 O(pat×str)O(|pat| \times |str|) 的上界。要留意的是它只覆盖 *?,且与本页走正则的实现是两条路——上一节实测的回溯爆炸正发生在正则那一路。它和 字符串匹配里的朴素扫描同源,只是多了一个 * 的「记忆点」。