☰
五分钟学会 CUE:learnxinyminutes-docs 配置语言实战指南
2026/10/5 6:50:31 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

CUE 是一种可导出为 JSON 或 YAML 的 JSON 超集配置语言,以其"类型即值"与统一(unification)引擎著称。本文以 learnxinyminutes-docs 仓库中的 cue.md 教学文档为骨架,从跨文件合并、冲突检测、默认值与析取,到定义、模板、包与模块,带你完整走一遍 CUE 的核心机制。读完本文,你将能够独立编写可校验、可复用的 CUE 配置文件,并用cue eval与cue export完成从 CUE 到 JSON/YAML 的实战转换。

CUE 是什么:表达力强但非图灵完备的 JSON 超集

CUE 是一种富有表现力、但非图灵完备(not Turing-complete)的 JSON 超集,可以导出为 JSON 或 YAML。它支持可选类型以及许多面向大规模配置集的处理便利特性。其统一引擎根植于逻辑编程(logic programming),因此为现代配置管理问题提供了现成的解决方案。这一设计定位非常关键:CUE 有意限制计算能力,以换取配置的可合并、可校验与可追溯,这正是它区别于通用编程语言的地方。

在 learnxinyminutes-docs 仓库中,cue.md 是 CUE 语言的完整速览文档,本文所有示例均继承自该文档,读者可在仓库根目录直接打开对照阅读。

跨文件统一:多个文件合并成一个对象

CUE 最核心的机制是统一(unification):当 CUE 导出为 JSON 时,每个被处理文件中的值都会被统一进一个巨大的顶层对象。考虑下面两个文件:

//name.cue name: "Daniel"
//disposition.cue disposition: "oblivious"

现在统一并导出为 JSON:

% cue export name.cue disposition.cue { "name": "Daniel", "disposition": "oblivious" }

也可以导出为 YAML:

% cue export --out yaml name.cue disposition.cue name: Daniel disposition: oblivious

从这个最小示例可以观察到三个细节:

  • C 风格的注释(//)不会出现在输出中;
  • CUE 语法中的键(key)通常不需要加引号;
  • 某些特殊字符仍然需要引号包裹,例如:
works_fine: true "needs-quotes": true

也就是说,只要键名符合普通标识符规则,就可以裸写;一旦包含-等特殊字符,就必须用双引号括起来。

全局合并:类型冲突与值冲突都会报错

统一不仅发生在文件之间,它同时也是所有类型和值的全局合并(global merge)。下面这个例子会失败,因为类型不同:

//string_value.cue foo: "baz"
//integer_value.cue foo: 100
% cue export string_value.cue integer_value.cue foo: conflicting values "baz" and 100 (mismatched types string and int): integer_value.cue:1:6 string_value.cue:1:6

注意错误信息末尾给出了文件名:行号:列号的精确位置,这在大规模配置中极具诊断价值。

即使把整数用引号包成字符串,仍然会失败,因为值冲突,且无法将所有内容统一进一个顶层对象:

//string_value.cue foo: "baz"
//integer_value.cue foo: "100" // a string now
% cue export string_value.cue integer_value.cue foo: conflicting values "100" and "baz": integer_value.cue:1:6 string_value.cue:1:6

可见,统一引擎对"类型不一致"和"具体值不一致"两种冲突都会给出明确的错误。这正是 CUE 能充当配置校验器的底层原因:任何两处对同一路径的矛盾定义都无法悄悄通过。

类型即值:schema、默认值与数据同源

CUE 中的类型本身就是值;统一引擎知道某些特殊类型的取值行为。在统一过程中,引擎要求具体值必须匹配指定类型;而当需要具体值(concrete value)却只有一个类型时,就会报错。所以下面这样是合法的:

street: "1 Infinite Loop" street: string

"1 Infinite Loop"满足string约束,二者统一成功。

cue export产出 YAML 或 JSON,而cue eval产出 CUE。这很适合把 YAML/JSON 转成 CUE,或直接用 CUE 检查统一后的输出。CUE 允许缺少具体值(在同时存在类型与匹配的具体值时,eval会优先输出具体值):

//type-only.cue amount: float
% cue eval type-only.cue amount: float

但如果想导出,就必须有具体值(或者用-c参数让eval强制要求具体值):

% cue export type-only.cue amount: incomplete value float

incomplete value是 CUE 中"只有类型没有具体值"时的典型报错。给这个类型统一一个具体值,一切就正常了:

//concrete-value.cue amount: 3.14
% cue export type-only.cue concrete-value.cue { "amount": 3.14 }

这种"用共享语法把具体值与类型统一起来"的方式非常强大,而且比 JSON Schema 紧凑得多。由此,schema、默认值和数据都可以用同一种语言——CUE——来表达,避免了传统方案中"定义与数据分离、易漂移"的问题。

默认值与析取(Disjunction)

类型可以附带默认值,用星号*标记:

// default-port.cue port: int | *8080
% cue eval default-port.cue port: 8080

int | *8080读作"整数,默认值 8080":|表示析取(disjunction),*标记其中的默认分支。当没有其他约束时,eval会选择默认值输出。

枚举式的选项也是析取,用|分隔:

//severity-enum.cue severity: "high" | "medium" | "low" severity: "unknown"
% cue eval severity-enum.cue severity: 3 errors in empty disjunction: severity: conflicting values "high" and "unknown": ./severity-enum.cue:1:11 ./severity-enum.cue:1:48 severity: conflicting values "low" and "unknown": ./severity-enum.cue:1:31 ./severity-enum.cue:1:48 severity: conflicting values "medium" and "unknown": ./severity-enum.cue:1:20 ./severity-enum.cue:1:48

"unknown"不在"high" | "medium" | "low"三个分支中,因此每个分支都与之冲突,最终得到 "empty disjunction"(空析取)错误。析取同样可以作用于结构体(struct),其行为与直觉一致——即结构体层面也可以做"多选一"的约束。

定义(Definitions)与&运算符

CUE 有"定义(definitions)"机制,可以像其他语言中的变量声明那样使用,也可以用来定义结构体类型。用&可以把一个结构体类型的定义应用到某些具体值上。还可以用[...#Whatever]表示"元素类型为 #Whatever 的列表":

// definitions.cue #DashboardPort: 1337 configs: { host: "localhost" port: #DashboardPort } #Address: { street: string city: string zip?: int // ? makes zip optional } some_address: #Address & { street: "1 Rocket Rd" city: "Hawthorne" } more_addresses: [...#Address] & [ {street: "1600 Amphitheatre Parkway", city: "Mountain View", zip: "94043"}, {street: "1 Hacker Way", city: "Menlo Park"} ]

导出结果:

% cue export --out yaml definitions.cue configs: host: localhost port: 1337 some_address: street: 1 Rocket Rd city: Hawthorne more_addresses: - street: 1600 Amphitheatre Parkway city: Mountain View zip: "94043" - street: 1 Hacker Way city: Menlo Park

要点拆解:

  • 以#开头的标识符(如#DashboardPort、#Address)是定义;#DashboardPort: 1337定义了一个常量,configs.port: #DashboardPort通过引用复用它;
  • #Address定义了结构体类型:street、city必须为字符串,zip用?标记为可选;
  • #Address & {...}表示把#Address的约束应用到具体值上——some_address因此获得了类型校验;
  • [...#Address] & [...]表示"列表中的每个元素都必须满足#Address"——注意第二个元素没有zip字段,但这合法,因为zip是可选的。

复杂值与更精细的校验

CUE 支持更复杂的值与校验,比如正则匹配与数值范围:

#Country: { name: =~"^\\p{Lu}" // Must start with an upper-case letter pop: >800 & <9_000_000_000 // More than 800, fewer than 9 billion } vatican_city: #Country & { name: "Vatican City" pop: 825 }
  • =~"^\\p{Lu}"表示name必须以大写字母(Unicode 属性Lu)开头;
  • >800 & <9_000_000_000表示pop必须大于 800 且小于 90 亿;数字中的下划线9_000_000_000只是可读性分隔符,会被正常解析;
  • 梵蒂冈城(人口 825、首字母大写)恰好满足全部约束,因此统一成功。

如果某个值不满足这些约束,统一引擎会像前面一样给出精确的冲突错误——配置的"安全网"由此建立。

路径语法糖:嵌套路径与集合约束

CUE 在纯 JSON 之上提供了大量语法糖,能省去不少样板代码。下面这段仅用三行就完成了对嵌套结构的"定义、'修改'与校验":

//paths.cue // path-value pairs outer: middle1: inner: 3 outer: middle2: inner: 7 // collection-constraint pair outer: [string]: inner: int
% cue export paths.cue { "outer": { "middle1": { "inner": 3 }, "middle2": { "inner": 7 } } }

这里有两个值得注意的点:

  • outer: middle1: inner: 3是路径-值对的简写,等价于嵌套的三层结构,免去了逐层书写花括号;
  • outer: [string]: inner: int是集合-约束对:[string]中的string被方括号包裹,是向引擎声明"这是一个类型约束,而不是字符串字面量"。它表示:outer下任意字符串键对应的结构,其inner字段都必须是整数。

正是这第三条约束,让middle1和middle2下的inner获得了类型校验——路径简写与约束校验在同一份文件里共存。

模板(Templates):单参数"函数"

与集合约束同源的还有模板(templates),它有点像一个单参数函数。下面Name被绑定到container下的每个字符串键,而键下对应的结构体随之被求值:

//templates.cue container: [Name=_]: { name: Name replicas: uint | *1 command: string } container: sidecar: command: "envoy" container: service: { command: "fibonacci" replicas: 2 }
% cue eval templates.cue container: { sidecar: { name: "sidecar" replicas: 1 command: "envoy" } service: { name: "service" command: "fibonacci" replicas: 2 } }

模板[Name=_]:的语义是:对container下的每一个键Name,应用该结构体约束。于是:

  • sidecar键被绑定为Name = "sidecar",name字段自动填入"sidecar",replicas未指定时取默认值1;
  • service键的replicas显式设为2,覆盖了默认值。

这是"用一条规则约束所有同类条目"的典型场景——新增一个容器只需一行具体值,模板自动补齐其余字段与校验。

作用域与引用(Scoped References)

CUE 支持作用域化引用。看这个例子:

//scopes-and-references.cue v: "top-level v" b: v // a reference a: { b: v // matches the top-level v } let V = v a: { v: "a's inner v" c: v // matches the inner v d: V // matches the top-level v now shadowed by a.v } av: a.v // matches a's v
% cue eval --out yaml scopes-and-references.cue
v: top-level v b: top-level v a: b: top-level v v: a's inner v c: a's inner v d: top-level v av: a's inner v

逐条解读:

  • b: v在顶层,引用顶层的v,得到"top-level v";
  • a.b: v虽然在a内部,但此时a内还没有自己的v,所以仍解析到顶层v;
  • let V = v在顶层把v的值"快照"进局部常量V;
  • 之后a内定义了a.v: "a's inner v",于是a.c: v就近解析到a内层的v(即"a's inner v"),而a.d: V仍引用被a.v遮蔽前的顶层值;
  • av: a.v显式写出路径,匹配a的v。

作者在文档中调整了输出键的顺序以方便阅读。键的顺序在 CUE 中并不重要,同时要注意:同一层级出现的重复键全部会被统一——这正是冲突检测与合并语义的体现。

隐藏字段(Hidden Fields)

以_为前缀的字段会被隐藏。如果需要导出的字段名真的以_开头,就用引号包起来:

//hiddens.cue "_foo": 2 _foo: 3 foo: 4 _#foo: 5 #foo : 6
% cue eval hiddens.cue "_foo": 2 foo: 4 #foo: 6 % cue export hiddens.cue { "_foo": 2, "foo": 4 }

对比eval与export的输出可以看清隐藏规则的分层:

  • _foo: 3是隐藏字段,eval和export都不输出;
  • "_foo": 2被引号包裹,是真实字段名,因此被输出;
  • _#foo: 5是隐藏的定义,eval中也不出现;
  • #foo: 6是普通定义:eval会显示(以便继续参与统一),而export不输出——因为导出的是面向消费者的数据,定义本身不属于数据。

插值(Interpolation):值与字段名都能插

CUE 支持对值和字段名做插值:

//interpolation.cue #expense: 90 #revenue: 100 message: "Your profit was $\( #revenue - #expense)" cat: { type: "Cuddly" "is\(type)": true }
% cue export interpolation.cue { "message": "Your profit was $10", "cat": { "type": "Cuddly", "isCuddly": true } }
  • 字符串内使用$\(表达式)进行值插值:#revenue - #expense被求值为10,嵌入message;
  • 字段名同样支持插值:"is\(type)"中type是cat内的字段,其值"Cuddly"被拼进字段名,生成isCuddly字段。

操作符、条件、列表推导与导入

CUE 还提供操作符、条件、列表推导以及导入等能力:

//getting-out-of-hand-now.cue import "strings" // we'll come back to this // operators are nice g: 5 / 3 // CUE can do math h: 3 * "blah" // and Python-like string repetition i: 3 * [1, 2, 3] // with lists too j: 8 < 10 // and supports boolean ops // conditionals are also nice price: number // Require a justification if price is too high if price > 100 { justification: string } price: 200 justification: "impulse buy" // list comprehensions are powerful and compact #items: [ 1, 2, 3, 4, 5, 6, 7, 8, 9] comp: [ for x in #items if x rem 2 == 0 {x*x}] // and... well you can do this too #a: [ "Apple", "Google", "SpaceX"] for k, v in #a { "\( strings.ToLower(v) )": { pos: k + 1 name: v nameLen: len(v) } }
% cue export getting-out-of-hand-now.cue
{ "g": 1.66666666666666666666667, "h": "blahblahblah", "i": [1, 2, 3, 1, 2, 3, 1, 2, 3], "j": true, "apple": { "pos": 1, "name": "Apple", "nameLen": 5 }, "google": { "pos": 2, "name": "Google", "nameLen": 6 }, "price": 200, "justification": "impulse buy", "comp": [ 4, 16, 36, 64 ], "spacex": { "pos": 3, "name": "SpaceX", "nameLen": 6 } }

逐个拆解:

  • 操作符:g: 5 / 3做精确除法(输出高精度小数);h: 3 * "blah"是类似 Python 的字符串重复;i: 3 * [1, 2, 3]对列表同样适用;j: 8 < 10是布尔比较;
  • 条件:if price > 100 { justification: string }声明"如果价格过高,则必须有理由字段";随后price: 200触发条件,justification: "impulse buy"满足约束;
  • 列表推导:for x in #items if x rem 2 == 0 {x*x}遍历 1~9,筛选偶数并求平方,得到[4, 16, 36, 64];
  • 导入与遍历生成字段:import "strings"后,for k, v in #a遍历公司名单,用strings.ToLower(v)生成小写键名,并用k + 1与len(v)填充pos、nameLen——配置可以"长出来",而不是手写。

需要提醒的是:正如文档作者所言,CUE 虽然不是图灵完备的,但它足够强大到让你搬起石头砸自己的脚。如果滥用条件、遍历与推导,配置会变得比手写更难维护。因此要保持配置清晰、善用注释,这也是 cue.md 全文反复强调的工程纪律。

包(Packages):同包文件自动统一

CUE 文件默认是**独立(standalone)**的;但如果文件顶部写上package子句,就表示该文件与"同一个包"内的其他文件可以互相统一:

//a.cue package config foo: 100 bar: int
//b.cue package config bar: 200

把这两个文件放进一个新目录后运行不带参数的cue eval,它们会如你所料被统一:CUE 会搜索当前目录下的.cue文件,若它们的包名相同,就统一求值。

模块(Modules):最大的组织单元

包的概念在"模块(modules)"的语境下更清晰。模块是最大的组织单元:基本上,只要一个项目跨越多个文件,就应该创建一个模块,并用类似 URL 域名加路径的形式命名,例如example.com/something。任何从该模块导入的内容——哪怕是模块内部导入——都必须使用完整的模块路径,且以模块名为前缀。

创建新模块:

mkdir mymodule && cd mymodule cue mod init example.com/mymodule

该命令会在mymodule目录下创建cue.mod/子目录,其中包含:

  • module.cue(定义模块名,本例为module: "example.com/mymodule")
  • pkg/
  • gen/
  • usr/

关于cue.mod内各目录的详细用途,CUE 官方文档的 packages 概念篇有更系统的说明;就日常使用而言,你几乎不需要关心该目录的内部细节,只需记住:模块名将成为模块内所有导入路径的前缀。

模块的文件层级都根植于mymodule/(即同时包含cue.mod/的那个目录)。要导入某个包,就用example.com/mymodule加上相对于mymodule/的路径作前缀。来看一个具体布局:

mymodule ├── config │ ├── a.cue │ └── b.cue ├── cue.mod │ ├── module.cue │ ├── pkg │ └── usr └── main.cue

cue.mod/及其下的文件由cue mod init example.com/mymodule生成;随后手工创建config/子目录放入a.cue、b.cue,再创建顶层文件main.cue。不带参数运行cue eval时,CUE 会检查当前目录下所有.cue文件是否属于同一个包——本例中只有main.cue声明了package main("main" 这个名字并无特殊含义,只是恰如其分),因此统一的就是它:

% cue eval configuredBar: 200

main.cue的内容是:

//main.cue package main import "example.com/mymodule/config" configuredBar: config.bar

而config/a.cue与config/b.cue就是前文那两个文件,只不过都加上了package config:

//a.cue package config foo: 100 bar: int
//b.cue package config bar: 200

导入语句import "example.com/mymodule/config"使用了完整模块路径,config.bar引用包导出的字段。若想验证config/下两个文件确实被统一了,可以把a.cue中的bar: int改成bar: string再运行cue eval,就会得到清晰的类型错误:

cue eval 2022-01-06 17:51:24 configuredBar: conflicting values string and 200 (mismatched types string and int): ./config/a.cue:4:6 ./config/b.cue:3:6 ./main.cue:5:16

错误信息同时点名了三个参与冲突的位置:类型定义处(a.cue)、具体值处(b.cue)以及引用处(main.cue),排查路径一目了然。文档作者还提到,cue.mod的设计为未来更多的包管理特性预留了空间。

内置模块:开箱即用的标准库

最后,CUE 自带功能强大的内置模块(built-in modules)。前文import "strings"并调用strings.ToLower就是其中一例。不带完整模块名的导入,默认视为内置模块。除strings外,内置模块还涵盖数学、正则、时间、编码等多种领域,各包完整清单与文档可在 CUE 官方的 Go 包文档站查阅(对应源码组织在 cuelang.org 的go/pkg目录下)。

在仓库中继续研读

本文是对 learnxinyminutes-docs 仓库 cue.md 的完整展开。该仓库是一个"whirlwind tours"(旋风式速览)系列教学库,如 README.md 所述,所有教程都以可运行的带注释代码形式呈现,边看代码边讲解;仓库的贡献规范也明确了这类文档的编写风格。若想深入 CUE 的官方教程与概念文档,可直接从 cue.md 文末的指引出发(其对应 CUE 官方 tutorials 与 packages 概念系列)。掌握了统一、类型即值、定义、模板、包与模块这些核心概念后,你便已具备把 CUE 用于真实项目配置管理的基础能力。

  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载
上一篇:Linux tar 命令完全指南:打包、压缩与解压实战手册
下一篇:Authelia 可观测性实战:Prometheus 指标导出(telemetry.metrics)配置与指标详解

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

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

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

立即咨询