正则表达式字符“$”并不表示“字符串结尾”
核心争议:$ 的含义
- 讨论围绕这样一个事实展开:在许多引擎中,非多行模式下的
$匹配的是字符串结尾 或者 终止\n之前的位置,而不是严格意义上的“字符串结尾”。 - 一些引擎(JS、Go、Rust、RE2 风格)在关闭多行模式时,将
$等同于“字符串结尾”。 - 另一些引擎(Python、Java、C#、PHP、Perl、PCRE 等)则把
$更像“行尾”来处理,并对单个尾随换行符做特殊处理。 - 几位评论者认为 Python/Perl 风格的行为令人意外且“古怪”,尤其是像
cat$不能匹配cat\n\n这样的例子。
行语义 vs 字符串语义
- 一个大的子线程争论
$概念上到底是“行尾”还是“字符串结尾”。 - 一方坚持,历史上的行导向视角(来自编辑器、
ed/sed/grep)足以解释为什么$会对尾随换行符作特殊处理。 - 另一方反驳说,大多数 API 接收的是任意字符串,而不是“行”,因此用户合理地会期待
$表示“字符串结尾”。 - 讨论还涉及 POSIX 对“行”“不完整行”的定义,以及换行符作为终止符还是分隔符应如何影响
$。
正则方言与(缺乏)标准化
- 很多人指出并不存在单一的正则标准;实际上有多个家族:
- POSIX BRE/ERE,锚点语义各不相同。
- Perl/PCRE 及其后代(PHP、许多语言、PCRE2)。
- RE2 风格,由 Go 和 Rust 的 regex crate 使用,刻意避免一些 Perl 的怪癖。
- JavaScript 的 ECMA 正则,也有自己的缺口和特性。
- 还提到了 C++
<regex>、ECMA-262、POSIX,以及一个较新的 RFC(I-Regexp)等正式规范,但它们是并存的,并没有统一行为。
安全性和正确性影响
$在尾随换行符之前也能匹配,已经导致过真实漏洞,例如 Ruby 输入验证和 ExifTool/GitLab 漏洞,攻击者可以在换行符后偷偷塞入载荷。- 一些评论中的建议是:当你真正想表达“整个字符串”时,在可用时应使用
\A...\Z或\z,而不是^...$。
工具、使用模式与易用性
- 许多人建议始终查看具体引擎的文档,并明确测试正则表达式。
- 有些人偏爱按行工作的工具(grep/sed/awk、Vim 之类编辑器),他们会以“行”为思维单位;另一些人主要处理内存中的字符串,因此期待字符串语义。
- 还普遍存在对正则方言碎片化、转义规则,以及字符类和锚点支持不一致的更广泛不满。