yq 除法运算符(/)完全指南:字符串分割与数值除法详解
2026/9/14 17:37:52 网站建设 项目流程

yq 除法运算符(/)完全指南:字符串分割与数值除法详解

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

导读

/(Divide)是 yq 中一个"一词两用"的二元运算符:当左侧操作数是字符串时,它执行字符串分割;当左侧操作数是整数或浮点数时,它执行算术除法。本文以 pkg/yqlib/doc/operators/divide.md 为主线,结合 operator_divide.go 的源码实现与 operator_divide_test.go 的测试用例,完整讲解/的三种典型场景(字符串分割、数值除法、除以零)、类型分派规则、边界错误场景与底层实现原理,让你在 YAML / JSON / XML / TOML 等任意格式的数据处理中准确驾驭这一运算符。

核心规则:行为取决于 LHS(左侧操作数)类型

Divide 运算符的行为由左侧操作数(LHS)的类型决定

  • 字符串(!!str:按右侧操作数(分隔符)进行字符串分割,结果为数组(序列);
  • 数字(!!int/!!float:进行算术除法,结果始终按浮点数计算;
  • 其他类型或类型不匹配:抛出错误。

这一点在源码中有清晰对应:divide函数首先检查 LHS 是否为!!null(null 不可参与除法),随后要求 LHS 与 RHS 均为标量(ScalarNode),再进入divideScalars依据具体标签分派(operator_divide.go)。

场景一:字符串分割(String split)

基本用法

给定sample.yml

a: cat_meow b: _

执行:

yq '.c = .a / .b' sample.yml

输出:

a: cat_meow b: _ c: - cat - meow

即:cat_meow按分隔符_被拆分为["cat", "meow"],并写入字段c。整个表达式可以理解为"把.a / .b的分割结果赋值给.c"。

底层实现:strings.Split

字符串分割的实现在 operator_strings.go 的split函数中,直接调用 Go 标准库strings.Split(value, spltStr)

func split(value string, spltStr string) (Kind, string, []*CandidateNode) { var contents []*CandidateNode if value != "" { var newStrings = strings.Split(value, spltStr) contents = make([]*CandidateNode, len(newStrings)) for index, str := range newStrings { contents[index] = &CandidateNode{Kind: ScalarNode, Tag: "!!str", Value: str} } } return SequenceNode, "!!seq", contents }

值得注意的实现细节:

  • 分割结果始终是一个!!seq(序列/数组),每个元素都是!!str(字符串)标量,即使分割出来的片段看起来像数字,也不会被隐式转换类型;
  • 若被分割的字符串为空,则返回空序列(无任何子节点);
  • 由于底层是strings.Split,其语义与 Go 标准库一致:空分隔符会把字符串拆成单个字符序列,分隔符不出现时返回包含整个原字符串的单元素序列;
  • 分隔符本身可以是任意表达式求值出的字符串,因此.a / .b.b可以来自 YAML 字段、环境变量或函数调用结果。

快速实践

# 直接输出分割结果(不写入字段) yq '.a / "-"' sample.yml # 按字符拆解字符串 yq '.name / ""' sample.yml

场景二:数值除法(Number division)

基本用法

数值除法过程中,结果始终按浮点数计算。

给定sample.yml

a: 12 b: 2.5

执行:

yq '.a = .a / .b' sample.yml

输出:

a: 4.8 b: 2.5

整数 12 除以浮点数 2.5,得到浮点结果 4.8,写入.a

底层实现:统一走 float64

divideScalars中(operator_divide.go),只要 LHS 与 RHS 的标签属于!!int!!float组合,就会把两侧都通过strconv.ParseFloat(value, 64)解析为float64后相除:

lhsNum, err := strconv.ParseFloat(lhs.Value, 64) rhsNum, err := strconv.ParseFloat(rhs.Value, 64) quotient := lhsNum / rhsNum target.Tag = "!!float" target.Value = fmt.Sprintf("%v", quotient)

由此可以总结出几条规则:

  • 结果标签恒为!!float:即使12 / 3得到整数 4,结果在 yq 中也会被标记为浮点类型(输出时可能显示为4,但类型语义是 float);
  • 除法结果不进行四舍五入或精度修饰,直接以fmt.Sprintf("%v", quotient)输出 float64 的自然表示;
  • 整数、浮点可以任意混合:int/intint/floatfloat/intfloat/float四种组合全部支持。

场景三:除以零(Division by zero)

基本用法

除以零不会报错,而是得到+Inf-Inf(正负无穷)。

给定sample.yml

a: 1 b: -1

执行:

yq '.a = .a / 0 | .b = .b / 0' sample.yml

输出:

a: +Inf b: -Inf

1 / 0得到+Inf-1 / 0得到-Inf。这是 float64 除法的标准 IEEE 754 语义:分子为正数得正无穷,分子为负数得负无穷。

底层实现

该行为同样来自strconv.ParseFloat+ float64 除法:Go 中浮点数除以零不会触发 panic,而是产生+Inf/-Inf(当分子不为零时)。随后fmt.Sprintf("%v", quotient)会将其格式化为+Inf/-Inf字面量,yq 将其作为浮点标量输出。

使用时的注意点:

  • 若你的下游工具不接受+Inf/-Inf,需要自行在表达式中用select或条件判断规避除零场景;
  • 0 / 0在 float64 语义下会产生NaN(Not a Number),在编写健壮表达式时同样需要考虑。

源码级原理:类型分派与错误处理

分派逻辑总览

divide的核心逻辑(operator_divide.go)可以概括为:

  1. LHS 标签为!!null→ 报错:"`!!null (path) cannot be divided by ...";
  2. LHS 与 RHS 必须都是标量节点,否则报错(map、seq 等容器类型不可参与除法);
  3. 两个标量进入divideScalars依据标签分派:
    • !!str/!!str→ 字符串分割(split);
    • !!int/!!float组合 → 浮点除法;
    • 其余组合 → 报错,例如!!int cannot be divided by !!str

自定义标签(Custom Tags)的处理

divideScalars对带自定义标签(如!horse!goat)的节点做了特殊处理(operator_divide.go):

  • 通过guessTagFromCustomType()猜测自定义标签底层的真实类型;
  • 若底层是字符串,则按字符串分割;若底层是数字,则按数值除法;
  • 数值除法时,若 LHS 带自定义标签,结果的标签会保留为 LHS 的自定义标签(如!horse),否则统一为!!float

对应测试(operator_divide_test.go)验证了!horse cat_meow / !goat _按字符串分割、!horse 1.2 / !goat 2.3按数值除法且结果保留!horse标签的行为。

运算符注册:优先级与词法

从 operation.go 可以看到/的注册定义:

var divideOpType = &operationType{Type: "DIVIDE", NumArgs: 2, Precedence: 42, Handler: divideOperator}
  • 二元运算符NumArgs: 2);
  • 优先级 42,与MULTIPLYMODULOADDSUBTRACT等算术运算符同级(operation.go);
  • 词法层面由 lexer_participle.go 的 token 规则{"Divide",/, opToken(divideOpType), 0}识别/符号并映射到该运算类型。

多节点组合语义

divideOperator的实现看,它通过crossFunction将 LHS 与 RHS 两侧所有匹配节点进行组合运算(operator_divide.go):

func divideOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { return crossFunction(d, context.ReadOnlyClone(), expressionNode, divide, false) }

这意味着当表达式左侧或右侧匹配到多个节点(如数组元素、递归遍历结果)时,/会对每一对 LHS/RHS 组合分别执行除法或分割——从源码结构看,这与 multiply 等算术运算符保持一致的分发模型。

边界场景与易错点

结合 operator_divide_test.go 的测试用例,以下场景会直接报错

场景示例输入错误信息
整数除以字符串a: 123,b: '2',表达式.a / .b!!int cannot be divided by !!str
字符串除以整数a: 2,b: '123',表达式.b / .a!!str cannot be divided by !!int
map 除以数字a: {"a":1},b: 2!!map (a) cannot be divided by !!int (b)
数组除以字符串a: [1,2],b: '2'!!seq (a) cannot be divided by !!str (b)
null 参与除法LHS 为!!null!!null (path) cannot be divided by ...

实践建议:

  • 字符串分割只接受字符串分隔符,数字分隔符需要先用tostring之类的转换,或使用split相关的其他操作;
  • 数值除法不接受字符串操作数,即使字符串内容看起来是数字(如'2'),也不会自动转换,必须先转为数值类型;
  • 容器类型(map / seq)不能直接参与除法
  • 除法运算不会改变原文档的其他字段=赋值只更新指定目标(测试中{a: 12, b: 2.5}除法后b保持不变)。

测试验证与运行方式

该运算符的完整行为由 operator_divide_test.go 中的divideOperatorScenarios表驱动测试覆盖,运行入口为TestDivideOperatorScenarios(operator_divide_test.go),涵盖:字符串分割、数值除法、除以零、自定义标签、锚点保留(如a: &horse [1]场景下除法后锚点仍被保留)、各类类型错误。测试同时通过documentOperatorScenarios机制生成并校验文档中的示例,保证 divide.md 与实现行为一致。

在仓库根目录运行以下命令即可复现验证:

go test ./pkg/yqlib/ -run TestDivideOperatorScenarios -v

延伸阅读

  • multiply-merge.md:同为高优先级算术运算符的*(乘/合并)行为;
  • modulo.md:优先级相同的%取模运算符;
  • string-operators.md:字符串相关的其他操作(如+拼接、split相关能力);
  • operators.md:完整运算符参考索引。

小结

yq 的/运算符是一个按左侧类型自动分派的"双面"操作符:对字符串执行分割(底层为strings.Split,产出!!seq),对数字执行 float64 算术除法(结果恒为!!float),除零时按 IEEE 754 语义返回+Inf/-Inf而非报错。理解它的类型分派规则与边界错误场景,能帮助你在复杂数据管道中安全、准确地使用字符串拆分与数值计算能力。

【免费下载链接】yqyq is a portable command-line YAML, JSON, XML, CSV, TOML, HCL and properties processor项目地址: https://gitcode.com/GitHub_Trending/yq/yq

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询