ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

Miller DSL 显式 r-string(r“...“)设计解析:让正则元字符穿越变量而不丢失 CLI数据分析【免费下载链接】millerMiller 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-stringr...的一份实现计划文档展开深入剖析正则位置字符串隐式 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 预处理而不是运行期值的某种属性regexProtectPrePassAuxpkg/dsl/cst/root.go#L234-L262在 CST 构建前遍历 AST把sub/gsub/regextract/regextract_or_else调用点以及~/!~操作符的第 2 个子节点从NodeTypeStringLiteral改标为NodeTypeRegex——但仅当该子节点本身就是一个字符串字面量 AST 节点时才会这样做。BuildLeafNodepkg/dsl/cst/leaves.go#L16-L135随后分派NodeTypeStringLiteral走BuildStringLiteralNode调用lib.UnbackslashStringLiteral见 pkg/lib/unbackslash.go#L38-L97把\t→TAB、\\→\等而NodeTypeRegex走BuildRegexLiteralNodepkg/dsl/cst/leaves.go#L270-L274不做任何 unescape直接把原始 lexeme 包进Mlrval。问题在于位于同一参数槽位的局部变量或字段不会被预处理触碰它不是字符串字面量 AST 节点所以它的运行期字符串值早已是普通 unescape 之后的结果——正则编译阶段已经没有信息可供撤销了。例如star_re \*;由于\*不是被识别的通用转义序列UnbackslashStringLiteral的未识别转义直接丢弃反斜杠行为见 pkg/lib/unbackslash.go#L89-L93会把\*变成单独的*字节——它本身就是一个无效/误导的正则片段。这也解释了为什么隐式 raw 只能作用于字面上的字符串字面量而无法跟随值穿过变量。正则编译的单一咽喉CompileMillerRegexpkg/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_literalb...语法中已存在单字母前缀带引号字面量pkg/parsing/mlr.bnf#L98-L99注释明确说明must precede non_sigil_name since b is an idchar——r前缀同样需要这个顺序约束因为r也是合法标识符字符。其 AST/CST 节点类型与构建器BuildBytesLiteralNodepkg/dsl/cst/leaves.go#L342-L357会剥掉前缀b和包围引号然后——与 r-string 期望相反——仍然调用UnbackslashStringLiteral字节语义的 unescape如\xff变成单字节 0xff。RegexCaseInsensitivepkg/parsing/mlr.bnf#L703-L705、pkg/dsl/cst/leaves.go#L58-L67可选尾部i后缀的先例。语法规则为string_literal non_sigil_nameCST 分派若发现字面量文本没有以i结尾就追加上i然后委托给正则字面量构建器后者保留 Mlrval 上的包围引号这样CompileMillerRegex的...i分支稍后能找到它们。前车之鉴旧提交不可复用Git 提交1230553ebregex r-string feature2021 年 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内容inputabc[.o*o.]def精确测试了 issue 中的隐式 r 场景gsub($input, \[, LEFT)等由提交b20a5ccd3/55209bfc5Test 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/StringLiteralpkg/parsing/mlr.bnf#L703-L709新增RStringCaseInsensitive与RStringLiteral两个产生式大小写不敏感变体声明在前与现有配对同约定# ra.*b (raw, case-sensitive) or ra.*bi (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 } ;并在MlrvalOrFunctionpkg/parsing/mlr.bnf#L639-L667中、紧挨现有RegexCaseInsensitive/StringLiteral/BytesLiteral行添加两个备选项CI 变体在前| RStringCaseInsensitive | RStringLiteral2. AST 节点类型pkg/dsl/ast_types.go靠近NodeTypeBytesLiteral/NodeTypeRegex约第 11-13 行新增两个 TNodeType 常量NodeTypeRStringLiteral TNodeType raw string literal NodeTypeRStringCaseInsensitive TNodeType case-insensitive raw string literalpkg/dsl/cst/ast_types.go普通形式靠近第 85 行、与文法type字符串一致CI 形式靠近第 115 行、与NodeTypeRegexCaseInsensitive并列新增两个 CST 常量NodeTypeRStringLiteral r_string_literal NodeTypeRStringCaseInsensitive RStringCaseInsensitive3. 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 cant 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 BuildRegexLiteralNodes representation, so CompileMillerRegexs // 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 CompileMillerRegexs 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中新增两个 caseCI 分支镜像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), nilregexProtectPrePassAux无需任何改动——它只改标已经是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) 头条场景变量携带 rawrstar r\*; $y gsub($x, rstar, star)(c)r...作为普通非正则值使用确认按 raw 打印/存储——例如ra\tb是 4 个字符a、\、t、b而不是一个 tab(d)r...i大小写不敏感匹配例如rabci ~ 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 形式保留引号的不对称性的说明有文档记载的权衡不是 bugr...i面向正则位置当作普通值使用时它会保留...i外壳与今天隐式...i正则字面量的行为一致。随后通过make -C docs/src forcebuild重建文档。验证清单make build编译通过手动检查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 } # 打印两个字符而不是一个 tabmake check单元 回归测试确认test/cases/dsl-regex-matching/0016与新增的 r-string 用例同时通过推送前执行make lint。小结一处只读的加法设计这个设计的精髓在于零侵入隐式 r-string 继续作为正则位置的默认行为受0016回归测试保护显式r...作为新的独立字面量类型叠加其上。由于CompileMillerRegex的 bare 回退分支早已支持无引号 raw 值整个实现只需要文法规则 两个 AST/CST 节点类型 一个不调用UnbackslashStringLiteral的构建器而正则编译路径、BIF 层和既有预处理全部原样保留——这正是 Issue #297 最后一步最稳妥的落地方式。赞分享CLI数据分析【免费下载链接】millerMiller 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点击查看免费下载相关推荐PRQL 字符串完全指南String 字面量、转义序列与 F/R/S 三种字符串格式详解PRQL 字符串完全指南String 字面量、转义序列与 F/R/S 三种字符串格式详解 PRQL 作为一门管道的 SQL 替代语言其字符串系统涵盖普通字符后端Miller 正则表达式完全指南~、strmatch/strmatchx 与 DSL 正则捕获机制Miller 正则表达式完全指南 ~ 、 strmatch / strmatchx 与 DSL 正则捕获机制 Miller一款面向 CSV、TSV、DKVCLI数据分析LocalSend元数据保留文件属性与创建时间保持原样LocalSend元数据保留文件属性与创建时间保持原样 痛点文件传输中的元数据丢失问题 在日常工作中你是否遇到过这样的困扰通过传统方式传输文件后文件的即时通讯网络/通信上一篇Upsonic 数据分析技能中的统计检验速查指南从检验选择、效应量到样本量与多重比较的完整实战参考下一篇Pencil免费开源GUI原型设计工具快速上手指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进