正则表达式字符“$”并不表示“字符串结尾”

正则表达式用户正在争论锚点 `$` 应该如何工作:很多人以为它表示“字符串结尾”,但在 Python、Java、.NET、PHP、Ruby 和 Perl 等语言中,它也可能匹配最后一个换行符之前的位置;而 JavaScript、Go、Rust 和 RE2 在关闭多行模式时则严格把它当作字符串结尾。评论者将其追溯到历史上的按行工具以及不同的正则方言(POSIX BRE/ERE、PCRE、RE2、自定义引擎),并指出这些微妙差异会在输入验证中造成真实的 bug 和安全问题。共识是:并不存在统一的正则标准,因此开发者必须阅读具体引擎的文档、编写有针对性的测试,并在真正需要字符串起止语义时尽量使用 `\A`/`\Z` 等锚点。

核心争议:$ 的含义

  • 讨论围绕这样一个事实展开:在许多引擎中,非多行模式下的 $ 匹配的是字符串结尾 或者 终止 \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 之类编辑器),他们会以“行”为思维单位;另一些人主要处理内存中的字符串,因此期待字符串语义。
  • 还普遍存在对正则方言碎片化、转义规则,以及字符类和锚点支持不一致的更广泛不满。