news 2026/9/25 15:54:41

Miller DSL 显式 r-string(r“...“)设计解析:让正则元字符穿越变量而不丢失

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Miller DSL 显式 r-string(r“...“)设计解析:让正则元字符穿越变量而不丢失
  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

项目地址:https://gitcode.com/gh_mirrors/mi/miller
点击查看免费下载

导读

本文围绕 Miller 开源项目中关于DSL 显式 r-string(r"...")的一份实现计划文档展开,深入剖析"正则位置字符串隐式 raw 化"这一历史机制为何无法跟随变量传递,以及如何通过新增一种独立字面量类型,让r"\*"既能直接用作正则参数、又能赋值给变量后再用于gsub/sub/=~等正则场景。读完本文,你将掌握 Miller DSL 中正则字面量的语法设计与编译管线(词法 → AST → CST → 运行期求值)、r"..."与r"..."i的语义与边界,以及如何在 pkg/parsing/mlr.bnf、pkg/dsl/cst/leaves.go 中落地这一能力并配套回归测试。

背景:Issue #297 遗留的最后一步

Miller 6 已经解决了 Issue #297("匹配正则元字符")的大部分问题,唯一的遗留诉求是显式 r-string:一种类似 Python raw string 的 DSL 字面量r"...",它不做任何反斜杠转义处理,并且关键的是——它必须能够赋值给变量、稍后在正则位置使用:

rstar = r"\*"; $y = gsub($x, rstar, "star");

这正是 issue 讨论串中 @torbiak 提出、@johnkerl 认可"今天无法工作"的场景。@johnkerl 倾向的设计(issue 中的 Option 1)是增量式的:保留今天"正则位置的普通字符串字面量"的隐式 r-string 行为,同时在其之上新增独立可用的显式r"..."字面量类型。

为什么变量无法携带"raw 性"

隐式 raw 是一个"解析期 AST 技巧",而非运行期值属性

普通字符串字面量之所以在"正则位置"(sub/gsub/regextract/regextract_or_else的第 2 个参数,以及=~/!=~的右侧)获得 raw(不转义)处理,靠的是一套parse-time AST 预处理,而不是运行期值的某种属性:

  • regexProtectPrePassAux(pkg/dsl/cst/root.go#L234-L262)在 CST 构建前遍历 AST,把sub/gsub/regextract/regextract_or_else调用点以及=~/!=~操作符的第 2 个子节点,从NodeTypeStringLiteral改标为NodeTypeRegex——但仅当该子节点本身就是一个字符串字面量 AST 节点时才会这样做。
  • BuildLeafNode(pkg/dsl/cst/leaves.go#L16-L135)随后分派:NodeTypeStringLiteral走BuildStringLiteralNode(调用lib.UnbackslashStringLiteral,见 pkg/lib/unbackslash.go#L38-L97,把\t→TAB、\\→\等);而NodeTypeRegex走BuildRegexLiteralNode(pkg/dsl/cst/leaves.go#L270-L274),不做任何 unescape,直接把原始 lexeme 包进Mlrval。

问题在于:位于同一参数槽位的局部变量或字段不会被预处理触碰(它不是字符串字面量 AST 节点),所以它的运行期字符串值早已是普通 unescape 之后的结果——正则编译阶段已经没有信息可供"撤销"了。例如:

star_re = "\*";

由于\*不是被识别的通用转义序列,UnbackslashStringLiteral的"未识别转义直接丢弃反斜杠"行为(见 pkg/lib/unbackslash.go#L89-L93)会把\*变成单独的*字节——它本身就是一个无效/误导的正则片段。这也解释了为什么隐式 raw 只能作用于"字面上的字符串字面量",而无法跟随值穿过变量。

正则编译的单一咽喉:CompileMillerRegex

pkg/lib/regex.go#L97-L125 的CompileMillerRegex是sub/gsub/regextract/regextract_or_else/strmatch/strmatchx/=~/!=~全部走到的唯一编译入口(由 pkg/bifs/regex.go 中的BIF_sub、BIF_gsub等调用)。其行为:

  • 若字符串以"开头并以"结尾,剥掉两端引号后交给 Goregexp.Compile;
  • 若以"..."i结尾,则转换为 Go 的(?i)前缀;
  • 若字符串完全没有包围引号,则原样直接传给regexp.Compile(第 124 行的 "bare" 回退分支)。

这个 bare 回退分支正是整个设计的承重事实:一个完全不带引号的 raw 字符串值(反斜杠保持原样、无外围引号),今天就能正确编译,regex.go和bifs/regex.go一行都不用改。因此只要显式 r-string 字面量在解析期构建出一个干净、不带引号的Mlrval,无论直接用作正则参数、还是通过变量传递,都能正确工作,编译路径完全无需改动。

可供参照的既有先例

设计文档在语法层面找到了三个现成模板:

  • bytes_literal(b"..."):语法中已存在单字母前缀带引号字面量(pkg/parsing/mlr.bnf#L98-L99,注释明确说明"must precede non_sigil_name since 'b' is an idchar"——r前缀同样需要这个顺序约束,因为r也是合法标识符字符)。其 AST/CST 节点类型与构建器BuildBytesLiteralNode(pkg/dsl/cst/leaves.go#L342-L357)会剥掉前缀b和包围引号,然后——与 r-string 期望相反——仍然调用UnbackslashStringLiteral(字节语义的 unescape,如\xff变成单字节 0xff)。
  • RegexCaseInsensitive(pkg/parsing/mlr.bnf#L703-L705、pkg/dsl/cst/leaves.go#L58-L67):可选尾部i后缀的先例。语法规则为string_literal non_sigil_name;CST 分派若发现字面量文本没有以i结尾就追加上"i",然后委托给正则字面量构建器,后者保留 Mlrval 上的包围引号,这样CompileMillerRegex的"..."i分支稍后能找到它们。

前车之鉴:旧提交不可复用

Git 提交1230553eb("regex r-string feature",2021 年 8 月)曾添加过regex_r_string ::= 'r' '"' {...} '"'并直接映射到NodeTypeRegex——但那是 Miller 6 重构之前的go/src/parsing/...树,该树在pkg/...重写中被删除、从未被带过来。其单元测试和文档当时就留作 TODO 且未勾选。虽然代码不可复用,但它确认了'r' '"' {...} '"'这一语法形状是站得住脚的。

文档已描述的隐式 r 行为

参考文档-正则表达式(约第 219-227 行)已经写明了隐式 r-string 现状:

"……(如果你熟悉 Python 的 r-string)所有处于正则位置的字符串都是隐式 r-string。一般来说这是正确且不易引起混淆的。但注意"\t"."\t"作为sub的第二个参数并不等同于"\t\t"。"

这里是补充显式r"..."文档最自然的位置。

钉死的回归测试:不要动

test/cases/dsl-regex-matching/0016(输入为 test/input/regex-metacharacters.dkvp,内容input=abc[.o*o.]def)精确测试了 issue 中的隐式 r 场景(gsub($input, "\[", "LEFT")等),由提交b20a5ccd3/55209bfc5("Test case for #297")加入。显式 r-string 的工作绝不能改变它的行为,它将继续作为隐式行为的回归护栏。

设计方案:从文法到 CST 构建

1. 文法(pkg/parsing/mlr.bnf)

在bytes_literal旁边、non_sigil_name之前新增r_string_literal词法规则(同样的顺序要求,因为r是合法标识符字符):

# Raw/r-strings r"..." (must precede non_sigil_name since 'r' is an idchar) r_string_literal ::= 'r' '"' { _string_char | _escape } '"' ;

紧挨RegexCaseInsensitive/StringLiteral(pkg/parsing/mlr.bnf#L703-L709)新增RStringCaseInsensitive与RStringLiteral两个产生式,大小写不敏感变体声明在前(与现有配对同约定):

# r"a.*b" (raw, case-sensitive) or r"a.*b"i (raw, case-insensitive). Must precede RStringLiteral. RStringCaseInsensitive ::= r_string_literal non_sigil_name -> { "parent": 0, "children": [0], "type": "RStringCaseInsensitive" } ; RStringLiteral ::= r_string_literal -> { "parent": 0, "children": [], "type": "r_string_literal" } ;

并在MlrvalOrFunction(pkg/parsing/mlr.bnf#L639-L667)中、紧挨现有RegexCaseInsensitive/StringLiteral/BytesLiteral行添加两个备选项(CI 变体在前):

| RStringCaseInsensitive | RStringLiteral

2. AST 节点类型

pkg/dsl/ast_types.go(靠近NodeTypeBytesLiteral/NodeTypeRegex,约第 11-13 行)新增两个 TNodeType 常量:

NodeTypeRStringLiteral TNodeType = "raw string literal" NodeTypeRStringCaseInsensitive TNodeType = "case-insensitive raw string literal"

pkg/dsl/cst/ast_types.go(普通形式靠近第 85 行、与文法"type"字符串一致;CI 形式靠近第 115 行、与NodeTypeRegexCaseInsensitive并列)新增两个 CST 常量:

NodeTypeRStringLiteral = "r_string_literal" NodeTypeRStringCaseInsensitive = "RStringCaseInsensitive"

3. CST 构建器(pkg/dsl/cst/leaves.go)

新增节点类型与构建器,仿照BuildBytesLiteralNode的前缀/引号剥离,但不调用UnbackslashStringLiteral:

// RStringLiteralNode is for explicit raw string literals r"..." (issue #297). // Unlike StringLiteralNode, no backslash processing is applied: r"\*" evaluates // to the two characters backslash-asterisk. This makes the value usable directly // as a regex-engine pattern fragment regardless of where it later travels -- as a // literal regex argument, or via a variable -- unlike the implicit-r-string trick // used for plain string literals in regex position (see regexProtectPrePassAux), // which only works at parse time and can't follow a value through a variable. type RStringLiteralNode struct { literal *mlrval.Mlrval } func (root *RootNode) BuildRStringLiteralNode(literal string) IEvaluable { // The PGPG lexer produces r_string_literal token with leading 'r' in the lexeme. if len(literal) >= 1 && literal[0] == 'r' { literal = literal[1:] } // Case-insensitive form r"..."i: leave the quotes and trailing 'i' intact, // matching BuildRegexLiteralNode's representation, so CompileMillerRegex's // existing "\"...\"i" handling applies unchanged. Case-insensitivity is only // meaningful once compiled as a regex, so this form is not intended to double // as a plain string value the way the non-CI form is. if len(literal) >= 3 && literal[0] == '"' && strings.HasSuffix(literal, "\"i") { return &RStringLiteralNode{literal: mlrval.FromString(literal)} } // Plain form r"...": strip the surrounding quotes for a clean raw-string value, // usable both as a regex argument (via CompileMillerRegex's bare-string // fallback) and as an ordinary string value. if len(literal) >= 2 && literal[0] == '"' && literal[len(literal)-1] == '"' { literal = literal[1 : len(literal)-1] } return &RStringLiteralNode{literal: mlrval.FromString(literal)} } func (node *RStringLiteralNode) Evaluate( state *runtime.State, ) *mlrval.Mlrval { return node.literal }

在BuildLeafNode的现有分派(pkg/dsl/cst/leaves.go#L42-L67)中新增两个 case,CI 分支镜像NodeTypeRegexCaseInsensitive的"缺少i则追加"模式:

case asts.NodeType(NodeTypeRStringLiteral): return root.BuildRStringLiteralNode(sval), nil case asts.NodeType(NodeTypeRStringCaseInsensitive): if sval == "" && astNode.Children != nil && len(astNode.Children) > 0 { sval = tokenLit(astNode.Children[0]) } if sval != "" && !strings.HasSuffix(sval, "i") { sval = sval + "i" } return root.BuildRStringLiteralNode(sval), nil

regexProtectPrePassAux无需任何改动——它只改标已经是NodeTypeStringLiteral的节点;语法原生的r"..."节点到达时已自带NodeTypeRStringLiteral/NodeTypeRStringCaseInsensitive标签,会原样穿过预处理。

4. 其他地方无需改动

pkg/lib/regex.go 与 pkg/bifs/regex.go 都不需要修改——正如上文"正则编译的单一咽喉"所分析的,bare 回退分支天然支持无引号 raw 值。

5. 重新生成解析器

pkg/parsing/mlr.bnf 经由 Miller 自己的 PGPG 生成器(github.com/johnkerl/pgpg,非 goyacc/lex),通过tools/build-dsl(需几分钟)重新生成 pkg/parsing/lexer/lexer.go 和 pkg/parsing/parser/parser.go。按 pkg/parsing/README.md 与 README-dev.md 的说明,这些生成文件是提交到版本库的——因此编辑mlr.bnf后要运行tools/build-dsl,并把重新生成的产物与文法改动一起提交。

6. 测试

在test/cases/dsl-regex-matching/下新增用例(沿用既有0016的结构),覆盖:

  • (a) 直接以r"\["风格字面量作为正则参数使用;
  • (b) 头条场景"变量携带 raw":rstar = r"\*"; $y = gsub($x, rstar, "star");
  • (c)r"..."作为普通非正则值使用,确认按 raw 打印/存储——例如r"a\tb"是 4 个字符(a、\、t、b),而不是一个 tab;
  • (d)r"..."i大小写不敏感匹配,例如r"abc"i =~ "ABC"。

test/cases/dsl-regex-matching/0016保持不动,作为既有隐式 r 行为的回归护栏。

7. 文档

扩展 docs/src/reference-main-regular-expressions.md.in 中现有的隐式 r-string 段落(约第 219-227 行),补充:

  • 新的显式r"..."语法;
  • issue 中的"变量携带 raw"示例(rstar = r"\*"; gsub($x, rstar, "star"));
  • 关于 CI 形式保留引号的不对称性的说明(有文档记载的权衡,不是 bug):r"..."i面向正则位置;当作普通值使用时它会保留"..."i外壳,与今天隐式"..."i正则字面量的行为一致。

随后通过make -C docs/src forcebuild重建文档。

验证清单

  1. make build编译通过;
  2. 手动检查:
echo 'a=[' | mlr put '$a = gsub($a, r"\[", "left_square")' echo 'a=*' | mlr put 'rstar = r"\*"; $a = gsub($a, rstar, "STAR")' mlr -n put 'end { print r"\t" }' # 打印两个字符,而不是一个 tab
  1. make check(单元 + 回归测试),确认test/cases/dsl-regex-matching/0016与新增的 r-string 用例同时通过;
  2. 推送前执行make lint。

小结:一处"只读"的加法设计

这个设计的精髓在于零侵入:隐式 r-string 继续作为正则位置的默认行为(受0016回归测试保护),显式r"..."作为新的独立字面量类型叠加其上。由于CompileMillerRegex的 bare 回退分支早已支持无引号 raw 值,整个实现只需要"文法规则 + 两个 AST/CST 节点类型 + 一个不调用UnbackslashStringLiteral的构建器",而正则编译路径、BIF 层和既有预处理全部原样保留——这正是 Issue #297 最后一步最稳妥的落地方式。

  • CLI
  • 数据分析

【免费下载链接】miller

Miller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON

项目地址:https://gitcode.com/gh_mirrors/mi/miller
点击查看免费下载

相关推荐

上一篇:Upsonic 数据分析技能中的统计检验速查指南:从检验选择、效应量到样本量与多重比较的完整实战参考
下一篇:Pencil:免费开源GUI原型设计工具快速上手指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 15:53:37

Trae 使用日志(挑刺版):Java + Maven 项目在 IDEA 里的配置踩坑记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 15:48:08

Claude Code 源码泄漏后,用 TaoToken 快速 fork 并验证配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 15:46:18

V100显卡sxm2版本引脚定义逆向

最近项目开发看到网上V100显卡sxm2版本引脚定义资源太少,就整理写一篇详细的V100显卡sxm2版本引脚定义也是帮助各大开发者快速入门:废话不多说,看下列图:本篇简单扼要,但是干货满满,以上两图都是我自己ecex…

作者头像 李华
网站建设 2026/9/25 15:40:29

CPU架构选型实战:x86_64、ARM64与龙架构的生态差异与避坑指南

1. 从一颗芯片说起:为什么你需要了解CPU架构如果你平时折腾开发板、装系统、跑容器,或者只是单纯想给自己换台电脑,那你大概率绕不开一个词——CPU芯片。这东西就像整个计算设备的大脑,决定了你能跑什么系统、用什么软件、性能天花…

作者头像 李华