- 文档
- 教程
【免费下载链接】learnxinyminutes-docs
Code documentation written as code! How novel and totally my idea!
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 floatincomplete 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: 8080int | *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.cuev: 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.cuecue.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: 200main.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!
相关推荐
五分钟速通 C 语言:learnxinyminutes-docs 官方速查指南全解析
五分钟速通 C 语言:learnxinyminutes docs 官方速查指南全解析 本篇技术指南以 learnxinyminutes docs 仓库中的 C
文档教程终极文言文编程入门:5分钟用古汉语写出你的第一个Hello World程序
终极文言文编程入门:5分钟用古汉语写出你的第一个Hello World程序 wenyan lang(文言文编程语言)是一个独特的开源项目,它让你能用古汉语语法编
编程语言编译器终极指南:5分钟学会tts-server-android多语言语音合成
终极指南:5分钟学会tts server android多语言语音合成 tts server android是一款功能强大的Android文本转语音应用,它支持
语音后端音频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考