設(shè)計(jì)解析:讓正則元字符穿越變量而不丟失)
CLI數(shù)據(jù)分析【免費(fèi)下載鏈接】millerMiller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON項(xiàng)目地址https://gitcode.com/gh_mirrors/mi/miller點(diǎn)擊查看免費(fèi)下載導(dǎo)讀本文圍繞 Miller 開源項(xiàng)目中關(guān)于DSL 顯式 r-stringr...的一份實(shí)現(xiàn)計(jì)劃文檔展開深入剖析正則位置字符串隱式 raw 化這一歷史機(jī)制為何無法跟隨變量傳遞以及如何通過新增一種獨(dú)立字面量類型讓r\*既能直接用作正則參數(shù)、又能賦值給變量后再用于gsub/sub/~等正則場景。讀完本文你將掌握 Miller DSL 中正則字面量的語法設(shè)計(jì)與編譯管線詞法 → AST → CST → 運(yùn)行期求值、r...與r...i的語義與邊界以及如何在 pkg/parsing/mlr.bnf、pkg/dsl/cst/leaves.go 中落地這一能力并配套回歸測試。背景Issue #297 遺留的最后一步Miller 6 已經(jīng)解決了 Issue #297匹配正則元字符的大部分問題唯一的遺留訴求是顯式 r-string一種類似 Python raw string 的 DSL 字面量r...它不做任何反斜杠轉(zhuǎn)義處理并且關(guān)鍵的是——它必須能夠賦值給變量、稍后在正則位置使用rstar r\*; $y gsub($x, rstar, star);這正是 issue 討論串中 torbiak 提出、johnkerl 認(rèn)可今天無法工作的場景。johnkerl 傾向的設(shè)計(jì)issue 中的 Option 1是增量式的保留今天正則位置的普通字符串字面量的隱式 r-string 行為同時(shí)在其之上新增獨(dú)立可用的顯式r...字面量類型。為什么變量無法攜帶raw 性隱式 raw 是一個(gè)解析期 AST 技巧而非運(yùn)行期值屬性普通字符串字面量之所以在正則位置sub/gsub/regextract/regextract_or_else的第 2 個(gè)參數(shù)以及~/!~的右側(cè)獲得 raw不轉(zhuǎn)義處理靠的是一套parse-time AST 預(yù)處理而不是運(yùn)行期值的某種屬性regexProtectPrePassAuxpkg/dsl/cst/root.go#L234-L262在 CST 構(gòu)建前遍歷 AST把sub/gsub/regextract/regextract_or_else調(diào)用點(diǎn)以及~/!~操作符的第 2 個(gè)子節(jié)點(diǎn)從NodeTypeStringLiteral改標(biāo)為NodeTypeRegex——但僅當(dāng)該子節(jié)點(diǎn)本身就是一個(gè)字符串字面量 AST 節(jié)點(diǎn)時(shí)才會(huì)這樣做。BuildLeafNodepkg/dsl/cst/leaves.go#L16-L135隨后分派NodeTypeStringLiteral走BuildStringLiteralNode調(diào)用lib.UnbackslashStringLiteral見 pkg/lib/unbackslash.go#L38-L97把\t→TAB、\\→\等而NodeTypeRegex走BuildRegexLiteralNodepkg/dsl/cst/leaves.go#L270-L274不做任何 unescape直接把原始 lexeme 包進(jìn)Mlrval。問題在于位于同一參數(shù)槽位的局部變量或字段不會(huì)被預(yù)處理觸碰它不是字符串字面量 AST 節(jié)點(diǎn)所以它的運(yùn)行期字符串值早已是普通 unescape 之后的結(jié)果——正則編譯階段已經(jīng)沒有信息可供撤銷了。例如star_re \*;由于\*不是被識(shí)別的通用轉(zhuǎn)義序列UnbackslashStringLiteral的未識(shí)別轉(zhuǎn)義直接丟棄反斜杠行為見 pkg/lib/unbackslash.go#L89-L93會(huì)把\*變成單獨(dú)的*字節(jié)——它本身就是一個(gè)無效/誤導(dǎo)的正則片段。這也解釋了為什么隱式 raw 只能作用于字面上的字符串字面量而無法跟隨值穿過變量。正則編譯的單一咽喉CompileMillerRegexpkg/lib/regex.go#L97-L125 的CompileMillerRegex是sub/gsub/regextract/regextract_or_else/strmatch/strmatchx/~/!~全部走到的唯一編譯入口由 pkg/bifs/regex.go 中的BIF_sub、BIF_gsub等調(diào)用。其行為若字符串以開頭并以結(jié)尾剝掉兩端引號(hào)后交給 Goregexp.Compile若以...i結(jié)尾則轉(zhuǎn)換為 Go 的(?i)前綴若字符串完全沒有包圍引號(hào)則原樣直接傳給regexp.Compile第 124 行的 bare 回退分支。這個(gè) bare 回退分支正是整個(gè)設(shè)計(jì)的承重事實(shí)一個(gè)完全不帶引號(hào)的 raw 字符串值反斜杠保持原樣、無外圍引號(hào)今天就能正確編譯regex.go和bifs/regex.go一行都不用改。因此只要顯式 r-string 字面量在解析期構(gòu)建出一個(gè)干凈、不帶引號(hào)的Mlrval無論直接用作正則參數(shù)、還是通過變量傳遞都能正確工作編譯路徑完全無需改動(dòng)??晒﹨⒄盏募扔邢壤O(shè)計(jì)文檔在語法層面找到了三個(gè)現(xiàn)成模板bytes_literalb...語法中已存在單字母前綴帶引號(hào)字面量pkg/parsing/mlr.bnf#L98-L99注釋明確說明must precede non_sigil_name since b is an idchar——r前綴同樣需要這個(gè)順序約束因?yàn)閞也是合法標(biāo)識(shí)符字符。其 AST/CST 節(jié)點(diǎn)類型與構(gòu)建器BuildBytesLiteralNodepkg/dsl/cst/leaves.go#L342-L357會(huì)剝掉前綴b和包圍引號(hào)然后——與 r-string 期望相反——仍然調(diào)用UnbackslashStringLiteral字節(jié)語義的 unescape如\xff變成單字節(jié) 0xff。RegexCaseInsensitivepkg/parsing/mlr.bnf#L703-L705、pkg/dsl/cst/leaves.go#L58-L67可選尾部i后綴的先例。語法規(guī)則為string_literal non_sigil_nameCST 分派若發(fā)現(xiàn)字面量文本沒有以i結(jié)尾就追加上i然后委托給正則字面量構(gòu)建器后者保留 Mlrval 上的包圍引號(hào)這樣CompileMillerRegex的...i分支稍后能找到它們。前車之鑒舊提交不可復(fù)用Git 提交1230553ebregex r-string feature2021 年 8 月曾添加過regex_r_string :: r {...} 并直接映射到NodeTypeRegex——但那是 Miller 6 重構(gòu)之前的go/src/parsing/...樹該樹在pkg/...重寫中被刪除、從未被帶過來。其單元測試和文檔當(dāng)時(shí)就留作 TODO 且未勾選。雖然代碼不可復(fù)用但它確認(rèn)了r {...} 這一語法形狀是站得住腳的。文檔已描述的隱式 r 行為參考文檔-正則表達(dá)式約第 219-227 行已經(jīng)寫明了隱式 r-string 現(xiàn)狀……如果你熟悉 Python 的 r-string所有處于正則位置的字符串都是隱式 r-string。一般來說這是正確且不易引起混淆的。但注意\t.\t作為sub的第二個(gè)參數(shù)并不等同于\t\t。這里是補(bǔ)充顯式r...文檔最自然的位置。釘死的回歸測試不要?jiǎng)觮est/cases/dsl-regex-matching/0016輸入為 test/input/regex-metacharacters.dkvp內(nèi)容inputabc[.o*o.]def精確測試了 issue 中的隱式 r 場景gsub($input, \[, LEFT)等由提交b20a5ccd3/55209bfc5Test case for #297加入。顯式 r-string 的工作絕不能改變它的行為它將繼續(xù)作為隱式行為的回歸護(hù)欄。設(shè)計(jì)方案從文法到 CST 構(gòu)建1. 文法pkg/parsing/mlr.bnf在bytes_literal旁邊、non_sigil_name之前新增r_string_literal詞法規(guī)則同樣的順序要求因?yàn)閞是合法標(biāo)識(shí)符字符# 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兩個(gè)產(chǎn)生式大小寫不敏感變體聲明在前與現(xiàn)有配對(duì)同約定# 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中、緊挨現(xiàn)有RegexCaseInsensitive/StringLiteral/BytesLiteral行添加兩個(gè)備選項(xiàng)CI 變體在前| RStringCaseInsensitive | RStringLiteral2. AST 節(jié)點(diǎn)類型pkg/dsl/ast_types.go靠近NodeTypeBytesLiteral/NodeTypeRegex約第 11-13 行新增兩個(gè) TNodeType 常量NodeTypeRStringLiteral TNodeType raw string literal NodeTypeRStringCaseInsensitive TNodeType case-insensitive raw string literalpkg/dsl/cst/ast_types.go普通形式靠近第 85 行、與文法type字符串一致CI 形式靠近第 115 行、與NodeTypeRegexCaseInsensitive并列新增兩個(gè) CST 常量NodeTypeRStringLiteral r_string_literal NodeTypeRStringCaseInsensitive RStringCaseInsensitive3. CST 構(gòu)建器pkg/dsl/cst/leaves.go新增節(jié)點(diǎn)類型與構(gòu)建器仿照BuildBytesLiteralNode的前綴/引號(hào)剝離但不調(diào)用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的現(xiàn)有分派pkg/dsl/cst/leaves.go#L42-L67中新增兩個(gè) 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無需任何改動(dòng)——它只改標(biāo)已經(jīng)是NodeTypeStringLiteral的節(jié)點(diǎn)語法原生的r...節(jié)點(diǎn)到達(dá)時(shí)已自帶NodeTypeRStringLiteral/NodeTypeRStringCaseInsensitive標(biāo)簽會(huì)原樣穿過預(yù)處理。4. 其他地方無需改動(dòng)pkg/lib/regex.go 與 pkg/bifs/regex.go 都不需要修改——正如上文正則編譯的單一咽喉所分析的bare 回退分支天然支持無引號(hào) raw 值。5. 重新生成解析器pkg/parsing/mlr.bnf 經(jīng)由 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后要運(yùn)行tools/build-dsl并把重新生成的產(chǎn)物與文法改動(dòng)一起提交。6. 測試在test/cases/dsl-regex-matching/下新增用例沿用既有0016的結(jié)構(gòu)覆蓋(a) 直接以r\[風(fēng)格字面量作為正則參數(shù)使用(b) 頭條場景變量攜帶 rawrstar r\*; $y gsub($x, rstar, star)(c)r...作為普通非正則值使用確認(rèn)按 raw 打印/存儲(chǔ)——例如ra\tb是 4 個(gè)字符a、\、t、b而不是一個(gè) tab(d)r...i大小寫不敏感匹配例如rabci ~ ABC。test/cases/dsl-regex-matching/0016保持不動(dòng)作為既有隱式 r 行為的回歸護(hù)欄。7. 文檔擴(kuò)展 docs/src/reference-main-regular-expressions.md.in 中現(xiàn)有的隱式 r-string 段落約第 219-227 行補(bǔ)充新的顯式r...語法issue 中的變量攜帶 raw示例rstar r\*; gsub($x, rstar, star)關(guān)于 CI 形式保留引號(hào)的不對(duì)稱性的說明有文檔記載的權(quán)衡不是 bugr...i面向正則位置當(dāng)作普通值使用時(shí)它會(huì)保留...i外殼與今天隱式...i正則字面量的行為一致。隨后通過make -C docs/src forcebuild重建文檔。驗(yàn)證清單make build編譯通過手動(dòng)檢查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 } # 打印兩個(gè)字符而不是一個(gè) tabmake check單元 回歸測試確認(rèn)test/cases/dsl-regex-matching/0016與新增的 r-string 用例同時(shí)通過推送前執(zhí)行make lint。小結(jié)一處只讀的加法設(shè)計(jì)這個(gè)設(shè)計(jì)的精髓在于零侵入隱式 r-string 繼續(xù)作為正則位置的默認(rèn)行為受0016回歸測試保護(hù)顯式r...作為新的獨(dú)立字面量類型疊加其上。由于CompileMillerRegex的 bare 回退分支早已支持無引號(hào) raw 值整個(gè)實(shí)現(xiàn)只需要文法規(guī)則 兩個(gè) AST/CST 節(jié)點(diǎn)類型 一個(gè)不調(diào)用UnbackslashStringLiteral的構(gòu)建器而正則編譯路徑、BIF 層和既有預(yù)處理全部原樣保留——這正是 Issue #297 最后一步最穩(wěn)妥的落地方式。贊分享CLI數(shù)據(jù)分析【免費(fèi)下載鏈接】millerMiller is like awk, sed, cut, join, and sort for name-indexed data such as CSV, TSV, and tabular JSON項(xiàng)目地址https://gitcode.com/gh_mirrors/mi/miller點(diǎn)擊查看免費(fèi)下載相關(guān)推薦PRQL 字符串完全指南String 字面量、轉(zhuǎn)義序列與 F/R/S 三種字符串格式詳解PRQL 字符串完全指南String 字面量、轉(zhuǎn)義序列與 F/R/S 三種字符串格式詳解 PRQL 作為一門管道的 SQL 替代語言其字符串系統(tǒng)涵蓋普通字符后端Miller 正則表達(dá)式完全指南~、strmatch/strmatchx 與 DSL 正則捕獲機(jī)制Miller 正則表達(dá)式完全指南 ~ 、 strmatch / strmatchx 與 DSL 正則捕獲機(jī)制 Miller一款面向 CSV、TSV、DKVCLI數(shù)據(jù)分析LocalSend元數(shù)據(jù)保留文件屬性與創(chuàng)建時(shí)間保持原樣LocalSend元數(shù)據(jù)保留文件屬性與創(chuàng)建時(shí)間保持原樣 痛點(diǎn)文件傳輸中的元數(shù)據(jù)丟失問題 在日常工作中你是否遇到過這樣的困擾通過傳統(tǒng)方式傳輸文件后文件的即時(shí)通訊網(wǎng)絡(luò)/通信上一篇Upsonic 數(shù)據(jù)分析技能中的統(tǒng)計(jì)檢驗(yàn)速查指南從檢驗(yàn)選擇、效應(yīng)量到樣本量與多重比較的完整實(shí)戰(zhàn)參考下一篇Pencil免費(fèi)開源GUI原型設(shè)計(jì)工具快速上手指南創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考