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

核心争议:$ 的含义

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