yq eval 运算符实战指南:动态执行表达式,让复杂脚本参数化
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
eval是 yq 中一个颇具“元编程”色彩的运算符:它接受一个参数,并把该参数当作一条yq 表达式重新解析、求值,而不是当作普通数据输出。它的典型应用场景是从环境变量、配置文件甚至命令行参数中动态注入表达式——把“查询/更新什么”这件事本身变成可配置的数据。读完本文,你将掌握eval的动态路径求值、与env/strenv组合完成环境变量驱动的更新操作,并理解它在 operator_eval.go 中的底层实现原理。
eval 是什么:把表达式当作数据来执行
按官方文档 eval.md 的定义:
Use
evalto dynamically process an expression - for instance from an environment variable.evaltakes a single argument, and evaluates that as ayqexpression.
也就是说,eval(...)括号内的内容不再只是普通的标量数据,而是一条独立的 yq 表达式。任何合法表达式都可以放进eval中,既可以是纯查询路径:
.a.b.c | select(. == "cat")也可以是更新表达式:
.a.b.c = "gogo"这为参数化复杂脚本提供了非常直接的手段:把表达式字符串存入环境变量或 YAML 字段,运行时再交给eval展开执行。
从源码角度看,eval的语法注册与操作符属性定义在 lexer_participle.go 和 operation.go:
// lexer_participle.go simpleOp("eval", evalOpType), // operation.go var evalOpType = &operationType{Type: "EVAL", NumArgs: 1, Precedence: 52, Handler: evalOperator, CheckForPostTraverse: true}其中NumArgs: 1表示它接收且仅接收一个参数,Handler: evalOperator指向真正的执行函数,Precedence: 52与map、filter、pick等一元运算符同级,CheckForPostTraverse: true表示它支持在求值后继续追加管道操作(例如eval(.pathExp)[])。
场景一:动态求值一个路径
最基础的用法是把一条查询路径存进文档字段,再用eval取出并执行。
给定sample.yml:
pathExp: .a.b[] | select(.name == "cat") a: b: - name: dog - name: cat执行:
yq 'eval(.pathExp)' sample.yml输出:
name: cat整个过程分两步理解:eval先读取字段pathExp的字符串值.a.b[] | select(.name == "cat"),随后把它当作表达式解析执行——遍历a.b数组并筛出name == "cat"的元素,最终得到{name: cat}这个 map 节点。
这一场景在 operator_eval_test.go 中有完全对应的测试用例:
{ description: "Dynamically evaluate a path", document: `{pathExp: '.a.b[] | select(.name == "cat")', a: {b: [{name: dog}, {name: cat}]}}`, expression: `eval(.pathExp)`, expected: []string{ "D0, P[a b 1], (!!map)::{name: cat}\n", }, },测试断言结果节点的路径为a b 1,即数组第二个元素,与文档示例完全吻合。
值得注意的是,eval的结果支持继续参与管道。测试中还覆盖了eval(.pathExp)[]这种“求值后再展开”的写法,输出会进一步下钻到标量cat(见 operator_eval_test.go),说明eval与其它运算符天然可以组合。
场景二:从环境变量动态更新路径
eval更常见的实战形态是与环境变量运算符搭配,实现“路径、值都来自运行时环境”的动态更新。官方文档给出的完整示例:
给定sample.yml:
a: b: - name: dog - name: cat执行:
pathEnv=".a.b[0].name" valueEnv="moo" yq 'eval(strenv(pathEnv)) = strenv(valueEnv)' sample.yml输出:
a: b: - name: moo - name: cat这条命令由两个对称的部分构成:
eval(strenv(pathEnv)):strenv(pathEnv)从环境变量pathEnv读出字符串.a.b[0].name,eval再把它解析为路径表达式——这是“写到哪里”;strenv(valueEnv):从环境变量valueEnv读出字符串moo——这是“写入什么值”;- 两者通过赋值运算符
=连接,构成一条完整的更新表达式。
环境变量可以是任意合法的 yq 表达式。对应测试见 operator_eval_test.go:
{ description: "Dynamically update a path from an environment variable", subdescription: "The env variable can be any valid yq expression.", document: `{a: {b: [{name: dog}, {name: cat}]}}`, environmentVariables: map[string]string{"pathEnv": ".a.b[0].name", "valueEnv": "moo"}, expression: `eval(strenv(pathEnv)) = strenv(valueEnv)`, expected: []string{ "D0, P[], (!!map)::{a: {b: [{name: moo}, {name: cat}]}}\n", }, },深入:env 与 strenv 的差异,以及参数化脚本的套路
在上面的命令中我们刻意使用了strenv而非env,二者的差异值得展开说明。从 operator_env.go 的实现看,envOperator读取环境变量后有两种处理分支:
- 当使用
strenv(StringValue: true)时,原始字符串被直接构造成!!str标量节点,原样保留; - 当使用
env时,字符串会被当作一段YAML 内容用NewYamlDecoder重新解码(operator_env.go),也就是说env支持把[1, 2]、{a: b}这类结构化文本变成真正的数组/对象。
因此在eval场景下必须用strenv:表达式文本.a.b[0].name本身只是一串字符,如果误用env,yq 会尝试把它当 YAML 解析,通常无法得到预期的路径字符串。
由此可以提炼出参数化复杂脚本的通用套路:
- 把“变化的表达式”抽成环境变量或配置文件字段,例如
pathEnv、filterExp、updateExpr; - 用
strenv(...)读取纯字符串形式的表达式; - 用
eval(...)把它升级为可执行的 yq 表达式; - 与其它运算符自由组合(管道、赋值、条件等)。
这样,同一份 yq 脚本可以在不修改命令本身的前提下,通过不同的环境变量注入不同的查询与更新逻辑,适合 CI/CD 流水线、批量配置改写等场景。更完整的环境变量运算符清单可参考 env-variable-operators.md。
底层原理:evalOperator 的两步求值
eval的核心执行逻辑在 operator_eval.go 的evalOperator函数中,可以拆解为两个阶段。
第一阶段:收集并解析表达式。先用GetMatchingNodes对expressionNode.RHS(即eval的参数)求值,得到若干匹配节点;随后遍历这些节点,把每个节点的值(expressionStrCandidate.Value)交给ExpressionParser.ParseExpression解析成真正的表达式 AST(operator_eval.go):
expressions[expIndex], err = ExpressionParser.ParseExpression(expressionStrCandidate.Value) if err != nil { return Context{}, err }注意这里对“每个匹配节点”都会解析:也就是说eval的参数如果匹配出多条记录,那么每条记录里的字符串都会被当作一条独立表达式。若字符串非法,ParseExpression会返回错误并中止执行——这正是文档中“任何合法表达式都可以使用”的保障机制。
第二阶段:对当前上下文中的每个节点执行每条表达式。双层循环把“当前匹配的文档节点”与“解析出的表达式”做交叉求值,结果统一收集后返回(operator_eval.go):
for matchEl := context.MatchingNodes.Front(); matchEl != nil; matchEl = matchEl.Next() { for expIndex = 0; expIndex < len(expressions); expIndex++ { result, err := d.GetMatchingNodes(context, expressions[expIndex]) ... results.PushBackList(result.MatchingNodes) } }从结构上可以推断:当eval参数匹配出多条表达式、且当前上下文又有多个节点时,执行结果是两者组合后的并集——这意味着eval天然支持“批量表达式”的语义,而不只是单条表达式的快捷方式。
边界与安全注意事项
- 表达式必须合法:
eval接收的字符串会被完整地走一遍解析器,任何语法错误都会直接导致命令失败并输出错误,不会静默忽略; - 与安全偏好联动:
eval本身依赖env/strenv读取环境变量,而 yq 的安全偏好配置中存在DisableEnvOps开关(见 security_prefs.go),当该选项被开启时,环境变量类运算会被拒绝(operator_env.go),使用eval配合环境变量的脚本此时将无法运行——在受控环境(如 CI 沙箱)中这是刻意的安全行为; - 适度使用:
eval的本质是“字符串 → 表达式”的动态编译,它带来了灵活性,但也让脚本的可读性与静态审查变差,建议只在确实需要参数化的地方(环境变量注入、多环境复用)使用。
小结
eval把 yq 从“固定表达式的处理器”升级为“可以按数据驱动执行表达式的处理器”:
- 用
eval(.someField)从文档内部读取并执行表达式,适合把查询逻辑放进配置; - 用
eval(strenv(envName))从环境变量注入路径或更新表达式,适合流水线与多环境场景; - 记住必须用
strenv而非env来保证表达式字符串原样传递; - 其实现是 operator_eval.go 中的“先解析字符串、再交叉执行”两步模型,并有 operator_eval_test.go 中的三类用例(动态路径、求值后展开、环境变量更新)作为行为契约。
掌握了eval,你就掌握了把 yq 表达式本身“数据化”的能力,这是编写可复用、可参数化配置处理脚本的关键一环。
【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考