yq 字符串操作符完全指南:match、capture、sub、interpolation 与 bash 换行实战
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
yq 除了解析与重构 YAML/JSON/XML 等结构化数据外,还提供了一组专门用于处理标量文本的操作符:正则匹配(match、capture、test)、替换(sub)、大小写转换(upcase/downcase)、拼接与拆分(join/split)、修剪(trim)与强制转字符串(to_string),以及表达式字符串插值(\(.expr))。本文基于仓库文档 string-operators.md 逐条展开所有用法与示例,并结合 operator_strings.go 等源码解析底层实现(Go 原生 RE2 正则、字符串插值器、类型守卫),帮助你既会写表达式,也明白这些操作在 yq 引擎内部是如何执行的。
一、字符串操作符总览
从操作符注册文件 operation.go 可以看到,字符串类操作符统一注册在 yq 的算子表中,每个算子都绑定了优先级(Precedence)与处理函数(Handler):
| 操作符 | 参数个数 | 功能 | 源码处理函数 |
|---|---|---|---|
match(regEx) | 1 | 返回子串匹配详情(可加"g"全局标志) | matchOperator |
capture(regEx) | 1 | 将命名捕获组输出为映射(map) | captureOperator |
test(regEx) | 1 | 返回true/false,类似 jq 的 test | testOperator |
sub(regEx, replacement) | 1(block) | 替换匹配到的子串,replacement 可引用捕获组 | substituteStringOperator |
upcase/ascii_upcase | 0 | 转大写,支持 Unicode | changeCaseOperator |
downcase/ascii_downcase | 0 | 转小写,支持 Unicode | changeCaseOperator |
join(sep) | 1 | 将数组用分隔符拼接为字符串 | joinStringOperator |
split(sep) | 1 | 将字符串按分隔符拆分为数组 | splitStringOperator |
trim | 0 | 去除首尾空白 | trimSpaceOperator |
to_string/to_str | 0 | 任意节点强制转为字符串 | toStringOperator |
"...\(expr)..." | - | 字符串插值 | stringInterpolationOperator |
其中upcase/downcase实际上是CHANGE_CASE操作符的两种配置别名,在词法分析器 lexer_participle.go 中通过changeCasePrefs{ToUpperCase: true/false}参数化注册,to_string同样接受to_str写法(to_?string正则)。
正则语法基础
所有正则类操作符底层都使用 Go 原生regexp包,即 RE2 语法(支持(?)修饰符、命名捕获组(?P<name>...)等),不支持回溯。
一个实用技巧:如需忽略大小写匹配,在正则前加(?i)前缀,例如test("(?i)cats")。这一点在源码中有明确的约束体现——extractMatchArguments(operator_strings.go)会显式拒绝match("cat"; "i")这种 jq 风格的"i"标志,并报错提示改用match("(?i)cat")写法。
二、match(regEx):获取子串匹配详情
match返回一个映射,包含string(匹配到的子串)、offset(起始偏移)、length(长度)和captures(捕获组列表)四个字段。
基本匹配
给定sample.yml:
foo bar fooyq 'match("foo")' sample.yml输出:
string: foo offset: 0 length: 3 captures: []不带全局标志时,match只返回第一次匹配。源码 getMatches 中的逻辑是:Global为false时调用FindStringSubmatch(单次匹配),为true时调用FindAllStringSubmatch/FindAllStringSubmatchIndex(全部匹配)。
全局标志 "g"
在第二个参数中传入"g"即可匹配全部出现,注意此时结果是多个映射,通常需要[...]收集成数组:
yq '[match("cat"; "g")]' sample.yml # sample.yml 内容为: cat cat- string: cat offset: 0 length: 3 captures: [] - string: cat offset: 4 length: 3 captures: []解析细节见 extractMatchArguments:如果第二个参数含字符g,则置matchPreferences.Global = true;出现i直接报错;其他无法识别的参数也会报错提示参考文档。
忽略大小写的匹配
yq '[match("(?i)foo"; "g")]' sample.yml # sample.yml 内容为: foo bar FOO- string: foo offset: 0 length: 3 captures: [] - string: FOO offset: 8 length: 3 captures: []捕获组(capture groups)
正则中的括号分组会逐个进入captures数组,每个捕获同样携带string/offset/length:
yq '[match("(ab)(c)"; "g")]' sample.yml # sample.yml 内容为: abc abc- string: abc offset: 0 length: 3 captures: - string: ab offset: 0 length: 2 - string: c offset: 2 length: 1 - string: abc offset: 4 length: 3 captures: - string: ab offset: 4 length: 2 - string: c offset: 6 length: 1命名捕获组
使用(?P<bar123>...)时,对应的捕获项会额外带上name字段:
yq '[match("foo (?P<bar123>bar)? foo"; "g")]' sample.yml # 内容为: foo bar foo foo foo- string: foo bar foo offset: 0 length: 11 captures: - string: bar offset: 4 length: 3 name: bar123 - string: foo foo offset: 12 length: 8 captures: - string: null offset: -1 length: 0 name: bar123注意第二个匹配中可选组未命中:源码 addMatch 中约定offset < 0表示该组没有匹配,此时string字段输出为null、offset为-1(与 jq 行为保持一致)。
类型守卫
match只能作用于字符串。源码 matchOperator 会用guessTagFromCustomType()检查节点 tag,非!!str时报错并给出提示:Hint: Most often you'll want to use '|=' over '=' for this operation——即提醒你在原地替换(|=)场景下才对当前标量做字符串操作。
三、capture(regEx):命名捕获组直接变 map
capture与match共用同一套参数解析(支持"g"),区别在于它把命名捕获组输出为一个映射,键即组名,值即捕获内容——在很多场景下比match更简洁。
yq 'capture("(?P<a>[a-z]+)-(?P<n>[0-9]+)")' sample.yml # 内容为: xyzzy-14a: xyzzy n: "14"注意n的值虽然看起来是数字,但 YAML 会保留其字符串属性(双引号输出)。源码实现见 capture:逐个命名组取出子匹配值,未命中的组(offset 为 -1)会写入null值(测试用例 operator_strings_test.go 中有对应验证:bar123: null)。
四、test(regEx):只返回布尔值
与 jq 的test语义一致,它按match的方式匹配,但只返回true/false,不输出完整匹配详情。
给定sample.yml:
- cat - dogyq '.[] | test("at")' sample.ymltrue false源码 testOperator 的实现非常直接:对每个候选节点调用regEx.FindStringSubmatch,用len(matches) > 0构造布尔节点。
五、sub(regEx, replacement):替换匹配的子串
sub替换字符串中所有匹配的子串。第一个参数是用于匹配的正则,第二个参数是替换内容,可以在替换内容中引用第一个正则的捕获组。
普通替换
yq '.a |= sub("dogs", "cats")' sample.yml # sample.yml 内容为: a: dogs are greata: cats are great注意这里使用|=(assign-update):在当前字符串值的上下文中执行替换后写回原路径,而不是新建一个结果。
带捕获组的替换
# sample.yml a: cat b: heatyq '.[] |= sub("(a)", "${1}r")' sample.ymla: cart b: heart替换串${1}r中的${1}引用了正则的第一个捕获组。源码 substitute 直接调用regex.ReplaceAllString(original, replacement),因此 Go 正则的全部替换语法(${1}、$name等)都可用。sub的两个参数分别作为 block 的 LHS/RHS 求值,见 getSubstituteParameters。
自定义标签的"伪字符串"
当 YAML 中出现自定义 tag(如!horse)时,yq 会尝试解码其底层类型。对底层是字符串的节点,字符串操作符依然有效,且 tag 会被保留:
# sample.yml a: !horse cat b: !goat heatyq '.[] |= sub("(a)", "${1}r")' sample.ymla: !horse cart b: !goat heart这在测试用例中有对应验证(operator_strings_test.go),也解释了为什么类型守卫用的是guessTagFromCustomType()而不是直接比较 tag——它会穿透自定义 tag 判断真实底层类型。
六、upcase / downcase:支持 Unicode 的大小写转换
upcase转大写、downcase转小写,均支持 Unicode 字符。
# sample.yml águayq 'upcase' sample.ymlÁGUA# sample.yml ÁgUAyq 'downcase' sample.ymlágua源码 changeCaseOperator 使用 Go 的strings.ToUpper/strings.ToLower,这两个函数本身是按 Unicode 逐 rune 处理的,所以á、Á等非 ASCII 字符也能正确转换。与upcase同注册的还有ascii_upcase别名(见 lexer_participle.go 的upcase|ascii_?upcase定义)。非字符串节点同样会被类型守卫拦截并报错。
七、join / split:字符串与数组的互转
join(sep):数组拼成字符串
# sample.yml - cat - meow - 1 - null - trueyq 'join("; ")' sample.ymlcat; meow; 1; ; true注意两点行为:null元素被替换为空串参与拼接(输出中1后面紧跟一个空段);join只能作用于数组,源码 joinStringOperator 检查node.Kind != SequenceNode时直接报错cannot join with ..., can only join arrays of scalars。
split(sep):字符串拆成数组
yq 'split("; ")' sample.yml # sample.yml 内容为: cat; meow; 1; ; true- cat - meow - "1" - "" - "true"拆出的每个元素都是字符串类型,所以1和true会被双引号包裹以保持字符串语义。若字符串中不存在分隔符,返回只含原字符串的单元素数组:
yq 'split("; ")' sample.yml # 内容为: word- word源码 split 基于 Go 的strings.Split(普通字符串分隔,不是正则);空字符串输入返回空序列,null节点则被直接跳过(splitStringOperator)。
八、trim:去除首尾空白
# sample.yml - ' cat' - 'dog ' - ' cow cow ' - horseyq '.[] | trim' sample.ymlcat dog cow cow horse源码 trimSpaceOperator 使用strings.TrimSpace去除首尾空白,同时保留原节点的 YAML 样式(Style);非字符串节点报错cannot trim ...。注意trim只影响标量的首尾空白,中间空白(如cow cow中间的空格)保持不变。
九、to_string:任意节点强制转为字符串
to_string(或to_str)把任意节点序列化为 YAML 文本字符串。
# sample.yml - 1 - true - null - ~ - cat - an: object - - array - 2yq '.[] |= to_string' sample.yml- "1" - "true" - "null" - "~" - cat - "an: object" - "- array\n- 2"从输出可以看出规则:原本就是字符串的cat保持原样;标量(1、true、null、~)变成带引号的字符串字面量;映射和数组则被编码成 YAML 片段字符串(an: object、含换行符的"- array\n- 2")。
实现见 toStringOperator:!!str节点原样保留;其他标量取node.Value;映射/序列则走 encodeToYamlString 按当前配置的缩进重新编码成字符串,并去掉末尾换行(chomper)。文档同时提醒:如果想让输出的标量保持引号包裹,可传--unwrapScalar=false或-r=f阻止 yq 输出时"拆包"纯字符串标量。
十、字符串插值(Interpolation):表达式里嵌入数据
在双引号字符串中,\(expression)会执行一个 yq 表达式并把结果拼入字符串。
给定sample.yml:
value: things another: stuffyq '.message = "I like \(.value) and \(.another)"' sample.yml输出:
value: things another: stuff message: I like things and stuff插值非字符串节点
当被插值的路径指向映射时,yq 会将其编码为 YAML 字符串再拼接:
# sample.yml value: an: appleyq '.message = "I like \(.value)"' sample.ymlvalue: an: apple message: 'I like an: apple'插值器的实现细节
插值的核心在 interpolate:它逐字符扫描字符串,遇到\\(进入表达式状态,用括号计数处理嵌套括号(支持\( (.value) )这种内部带括号的表达式);遇到不匹配的)则按普通字符处理;遇到未闭合的插值会打印告警unclosed interpolation string, skipping interpolation并原样输出字符串——测试用例 operator_strings_test.go 覆盖了"未闭合插值"与"转义导致未闭合"两种边界。
其他由测试用例确认的行为:
- 不插值:
"Hi (.value)"(没有反斜杠)原样输出Hi (.value); - 转义:
"Hi \\(.value)"中的插值被转义,输出字面量Hi \(.value); - 全局开关:operator_strings.go 中的
StringInterpolationEnabled为false时,stringInterpolationOperator直接把字符串当作纯文本处理,不再求值。
十一、Bash 换行、字符串块与 strenv
Bash 会吃掉宝贵的末尾换行符,导致设置带换行的字符串很棘手。尤其是$( exp )命令替换会裁剪末尾换行。
例如要得到这样的 YAML:
a: | cat用$( ... )是不行的,因为末尾换行会被裁掉:
m=$(echo "cat\n") yq -n '.a = strenv(m)' # 输出 a: cat而用printf -v可以保住换行:
printf -v m "cat\n" ; m="$m" yq -n '.a = strenv(m)' # 输出 a: | cat同样可以使用多行字符串变量:
m="cat " yq -n '.a = strenv(m)' # 输出 a: | cat如果要从文件读取内容并希望保留末尾换行,推荐这种读法:
IFS= read -rd '' output < <(cat my_file) output=$output ./yq '.data.values = strenv(output)' first.yml其中用到的strenv(name)是环境变量操作符,用于把环境变量以字符串形式引入表达式。它在词法层通过专门的 token 识别(lexer_participle.go 中strenv\([^\)]+\)直接匹配strenv(变量名)写法),实现见 operator_env.go。文档中strenv(m)的参数是裸标识符(非引号字符串),这正是其词法定义所支持的用法。需要注意:当启用安全模式(security mode)时,strenv会因涉及系统环境变量访问而被拒绝,相关限制在 operator_env_test.go 中有对应测试。
十二、文档与源码的对应关系:测试即文档
一个值得注意的工程细节:本文依据的文档 string-operators.md 中的每个"输入→表达式→输出"示例,都能在测试文件 operator_strings_test.go 的stringsOperatorScenarios表里找到逐字对应项(如match("foo")、[match("cat"; "g")]、.a |= sub("dogs", "cats")、.[] |= to_string等),且测试函数最后调用documentOperatorScenarios(t, "string-operators", ...)把文档与测试场景做一致性校验。
这意味着:
- 文档中的每一个输出示例都是经过自动化测试验证的真实行为,而非手写的示意;
- 测试表里还包含大量
skipDoc: true的补充场景(自定义 tag、未命中匹配返回空、split("; ")[]展开、to_string的行内数组写法等),覆盖范围比文档展示的更广; - 如果你发现某个示例在当前版本运行结果不同,优先以该测试文件的预期值为准,它就是行为契约。
总结
yq 的字符串操作符围绕一条清晰的主线展开:RE2 正则三件套(match/capture/test)负责"看",sub负责"改",join/split/trim/upcase/downcase/to_string负责"整理",插值与strenv负责把外部数据"注入"表达式。所有操作符都通过guessTagFromCustomType做字符串类型守卫(因此自定义 tag 的字符串节点也能正常处理),并注册在 operation.go 的算子表中。日常使用时记住两个高频技巧即可:全局匹配加"g"([match("pat"; "g")])、忽略大小写加(?i)前缀;而带换行的字符串注入则优先用printf -v或IFS= read -rd ''保住末尾换行,再经strenv写入 YAML。
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考