news 2026/9/14 15:09:20

yq 字符串操作符完全指南:match、capture、sub、interpolation 与 bash 换行实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
yq 字符串操作符完全指南:match、capture、sub、interpolation 与 bash 换行实战

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 等结构化数据外,还提供了一组专门用于处理标量文本的操作符:正则匹配(matchcapturetest)、替换(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 的 testtestOperator
sub(regEx, replacement)1(block)替换匹配到的子串,replacement 可引用捕获组substituteStringOperator
upcase/ascii_upcase0转大写,支持 UnicodechangeCaseOperator
downcase/ascii_downcase0转小写,支持 UnicodechangeCaseOperator
join(sep)1将数组用分隔符拼接为字符串joinStringOperator
split(sep)1将字符串按分隔符拆分为数组splitStringOperator
trim0去除首尾空白trimSpaceOperator
to_string/to_str0任意节点强制转为字符串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 foo
yq 'match("foo")' sample.yml

输出:

string: foo offset: 0 length: 3 captures: []

不带全局标志时,match只返回第一次匹配。源码 getMatches 中的逻辑是:Globalfalse时调用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字段输出为nulloffset-1(与 jq 行为保持一致)。

类型守卫

match只能作用于字符串。源码 matchOperator 会用guessTagFromCustomType()检查节点 tag,非!!str时报错并给出提示:Hint: Most often you'll want to use '|=' over '=' for this operation——即提醒你在原地替换(|=)场景下才对当前标量做字符串操作。

三、capture(regEx):命名捕获组直接变 map

capturematch共用同一套参数解析(支持"g"),区别在于它把命名捕获组输出为一个映射,键即组名,值即捕获内容——在很多场景下比match更简洁。

yq 'capture("(?P<a>[a-z]+)-(?P<n>[0-9]+)")' sample.yml # 内容为: xyzzy-14
a: 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 - dog
yq '.[] | test("at")' sample.yml
true false

源码 testOperator 的实现非常直接:对每个候选节点调用regEx.FindStringSubmatch,用len(matches) > 0构造布尔节点。

五、sub(regEx, replacement):替换匹配的子串

sub替换字符串中所有匹配的子串。第一个参数是用于匹配的正则,第二个参数是替换内容,可以在替换内容中引用第一个正则的捕获组。

普通替换

yq '.a |= sub("dogs", "cats")' sample.yml # sample.yml 内容为: a: dogs are great
a: cats are great

注意这里使用|=(assign-update):在当前字符串值的上下文中执行替换后写回原路径,而不是新建一个结果。

带捕获组的替换

# sample.yml a: cat b: heat
yq '.[] |= sub("(a)", "${1}r")' sample.yml
a: 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 heat
yq '.[] |= sub("(a)", "${1}r")' sample.yml
a: !horse cart b: !goat heart

这在测试用例中有对应验证(operator_strings_test.go),也解释了为什么类型守卫用的是guessTagFromCustomType()而不是直接比较 tag——它会穿透自定义 tag 判断真实底层类型。

六、upcase / downcase:支持 Unicode 的大小写转换

upcase转大写、downcase转小写,均支持 Unicode 字符。

# sample.yml água
yq 'upcase' sample.yml
ÁGUA
# sample.yml ÁgUA
yq '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 - true
yq 'join("; ")' sample.yml
cat; 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"

拆出的每个元素都是字符串类型,所以1true会被双引号包裹以保持字符串语义。若字符串中不存在分隔符,返回只含原字符串的单元素数组:

yq 'split("; ")' sample.yml # 内容为: word
- word

源码 split 基于 Go 的strings.Split(普通字符串分隔,不是正则);空字符串输入返回空序列,null节点则被直接跳过(splitStringOperator)。

八、trim:去除首尾空白

# sample.yml - ' cat' - 'dog ' - ' cow cow ' - horse
yq '.[] | trim' sample.yml
cat 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 - 2
yq '.[] |= to_string' sample.yml
- "1" - "true" - "null" - "~" - cat - "an: object" - "- array\n- 2"

从输出可以看出规则:原本就是字符串的cat保持原样;标量(1truenull~)变成带引号的字符串字面量;映射和数组则被编码成 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: stuff
yq '.message = "I like \(.value) and \(.another)"' sample.yml

输出:

value: things another: stuff message: I like things and stuff

插值非字符串节点

当被插值的路径指向映射时,yq 会将其编码为 YAML 字符串再拼接:

# sample.yml value: an: apple
yq '.message = "I like \(.value)"' sample.yml
value: 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 中的StringInterpolationEnabledfalse时,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 -vIFS= 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),仅供参考

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

企业知识库搭建工具怎么选?从知识管理到团队协作一次讲透

我见过太多人把"企业知识库搭建工具"这个事儿想简单了&#xff0c;以为上个Notion、开个共享文档就完事了。结果呢&#xff1f;用三个月&#xff0c;里头全是陈年旧档、重复资料和离职同事留下的"烂尾楼"&#xff0c;搜索框形同虚设&#xff0c;最后变成一…

作者头像 李华
网站建设 2026/9/14 15:05:59

2026年上位机选型指南:C#、LabVIEW与Qt三大路线解析

1. 为什么2026年的上位机选型&#xff0c;反而比十年前更难了先说个反直觉的现象&#xff1a;十年前做上位机&#xff0c;根本不需要纠结选型。那时候工控现场清一色是组态软件&#xff0c;或者谁熟用什么就上什么。但到了2026年&#xff0c;我收到的私信里十有八九都在问同一个…

作者头像 李华
网站建设 2026/9/14 15:05:14

从个人效率到组织智能:企业级Agent平台关键能力与落地实践

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

作者头像 李华
网站建设 2026/9/14 15:04:54

STM32F103ZET6+STemWin实现GIF动图显示的完整例程与内存优化

简介&#xff1a;STM32F103ZET6单片机STemWin-GIF图片显示实验例程源码&#xff0c;面向使用该型号单片机的嵌入式开发者&#xff0c;演示如何在STemWin图形库中加载与播放GIF动态图片。例程涵盖LCD显示初始化、外设配置、STemWin库初始化等流程&#xff0c;并展示图片尺寸调整…

作者头像 李华
网站建设 2026/9/14 15:04:33

Fluent许可证成本分摊模型与优化实践

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

作者头像 李华