Cue-Lang介绍
Cue,是一种开源语言,用于定义,生成和验证各种数据:配置,API,数据库模式,代码……。它能够将数据的结构、约束、数值作为同一层级成员,从而简化配置文件的生成。
Cue教程
Cue格式说明
- 使用
//
进行单行注释 - 对象被称为结构体
- 对象成员称为结构字段
- 对于没有特殊字符的字段名,可以省略引号
- 结构字段后面无需
,
- 在列表中的最后一个元素后放置
,
- 最外层的
{}
可省略
例子:
1 | str: "hello world" |
Cue 结构、约束、数据
1 | // 结构 |
1 | // 约束 |
1 | // 数据 |
Cue的最佳实践:从开放的结构模式开始,限制上下文可能性,最终具体到数据实例。
Cue哲学:为了保证唯一性,Cue的数据不会被覆盖。
Cue核心规则
- 数据可被重复定义,但必须值保持一致
- 结构字段可以被更强限制覆盖
- 结构的字段会被合并,如果是列表,必须严格匹配
- 规则可被递规应用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19hello: "world"
hello: "world"
// set a type
s: { a: int }
// set some data
s: { a: 1, b: 2 }
// set a nested field without curly braces
s: c: d: 3
// lists must have the same elements
// and cannot change length
l: ["abc", "123"]
l: [
"abc",
"123"
]
结构
- 结构并不会输出
- 它的值可能是不确认、不完整的
- 字段必须完全
使用#mydef
来定义结构,使用...
来定义一个开放的结构体
1 | #Album: { |
1 | #Person: { |
约束
约束与数值使用&
字符进行连接时,会将值进行校验
1 | // conjunctions on a field |
替换
使用|
可以实现支持多种结构。同时它也可以为出错值设置替换值
1 | // disjunction of values (like an enum) |
默认值与可选
使用*
来设置默认值, ?
设置可选字段
1 | s: { |
开放模式与封闭模式
开放模式意味着结构可以扩展,关闭模式意味着不能扩展。 默认情况下,结构是开放模式,定义是封闭模式。 可以通过定义的最后添加...
来申明开放模式定义;另外通过过close强制为结构体设置为关闭模式
1 | // Open definition |
推荐从基础定义开始,复用定义
在编写Cue时,推荐从基础定义开始,这样能够有更好的复用能力。
1 | #Base: { |
定义多行字符串
- 使用
"""
来定义多行字符串1
2
3
4
5str1: #"avoid using \ to "escape""#
str2: """
a nested multiline
string goes here
""" - 使用反引号(`)定义原始字符串
1
2
3
4
5multiline: `
这是一个
多行字符串
保留了换行和空格
` - 使用#”…”# 定义原始字符串,可以避免转义
1
2
3
4multiline: #"
这种写法可以包含 "引号" 而不需要转义
还可以包含 \反斜杠\ 等特殊字符
"# - 使用 + 连接多个字符串
1
2
3multiline: "第一行\n" +
"第二行\n" +
"第三行"
List
List 可被定义为开放模式,这样便可与其它数据进行合并,
1 | empty: [] |
Struct
结构体是Cue的主要内容,也是最终数据的输出。如上介绍,默认情况下它是开放模式。除了使用Json类型形式进行设置值,还可通过级联:
来设置,如a: hello: "world"
1 | // an open struct |
模式匹配约束
模式匹配允许您为与模式匹配的标签指定约束。可以将约束应用于字符串标签,并使用标识符来设置字段。
1 | #schema: { |
表达式
- 引用字段,使用
\(**)
显用其它字段1
2
3
4
5
6
7
8
9
10
11
12
13container: {
repo: "docker.io/cuelang"
image: "cue"
version: "v0.3.0"
full: "\(repo)/\(image):\(version)"
}
name: "Tony"
msg: "Hello \(name)"
// conver string to bytes
b: '\(msg)'
// convert bytes to string
s: "\(b)" - Cue也能够为通过
\(**)
来设置key1
2
3
4
5
6
7
8
9
10
11
12
13apps: ["nginx", "express", "postgres"]
#labels: [string]: string
stack: {
for i, app in apps {
"\(app)": {
name: app
labels: #labels & {
app: "foo"
tier: "\(i)"
}
}
}
} - List遍历
遍历List数据格式如下:[ for key, val in <iterable> [condition] { production } ]
1
2
3
4
5
6
7
8
9
10nums: [1,2,3,4,5,6]
sqrd: [ for _, n in nums { n*n } ]
even: [ for _, n in nums if mod(n,2) == 0 { n } ]
listOfStructs: [ for p, n in nums {
pos: p
val: n
}]
extractVals: [ for p, S in listOfStructs { S.val } ] - 条件控制语句
没有else,所有判断都会被执行1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22app: {
name: string
tech: string
mem: int
if tech == "react" {
tier: "frontend"
}
if tech != "react" {
tier: "backend"
}
if mem < 1Gi {
footprint: "small"
}
if mem >= 1Gi && mem < 4Gi {
footprint: "medium"
}
if mem >= 4Gi {
footprint: "large"
}
}
标准库
Cue的标准库中包含了很多的帮助包(helper packages)。
- Encoding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25package stdlib
import (
"encoding/json"
)
data: """
{
"hello": "world",
"list": [ 1, 2 ],
"nested": {
"foo": "bar"
}
}
"""
jval: json.Unmarshal(data)
val: {
hello: "world"
list: [1,2]
nested: foo: "bar"
}
cjson: json.Marshal(val) - Strings
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package stdlib
import "strings"
s: "HelloWorld"
u: strings.ToUpper(s)
l: strings.ToLower(s)
line: "Cue stands for configure, unify, execute"
words: strings.Split(line, " ")
lined: strings.Join(words, " ")
haspre: strings.HasPrefix(line, "Cue")
index: strings.Index(line, "unify") - List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package stdlib
import "list"
l1: [1,2,3,4,5]
l2: ["c","b","a"]
// constrain length
l2: list.MinItems(1)
l2: list.MaxItems(3)
// slice a list
l3: list.Slice(l1, 2,4)
// get the sum and product
sum: list.Sum(l1)
prd: list.Product(l1)
// linear search for list (no binary)
lc: list.Contains(l1, 2)
// sort a list
ls: list.Sort(l2, list.Ascending)
l2s: list.IsSorted(l2, list.Ascending)
lss: list.IsSorted(ls, list.Ascending)
// Flatten a list
ll: [1,[2,3],[4,[5]]]
lf: list.FlattenN(ll, 1) - Constrain
1
2
3
4
5
6
7
8
9
10
11
12
13
14package stdlib
import (
"net"
"time"
)
// string with ip format
ip: net.IPv4
ip: "10.1.2.3"
// string with time format
ts: time.Format(time.ANSIC)
ts: "Mon Jan 2 15:04:05 2006"
模块和包
cuelang有module和package系统,可以import依赖
- 模块定义
- 通过在项目根目录创建cue.mod/module.cue文件来定义模块。通过
cue mod init <模块名>
来初始化。 - 模块名格式通常为
domain.com/name
或github.com/owner/repo
- package组织
- 一个模块可以包含多个package
- 允许在一个目录中包含多个package
- 导入package
- 使用绝对路径导入,不允许相对路径导入
- 导入时可以省略domain,表示导入内置标准包
- 可以在导入时重命名包
- 同一个包内的定义和值可以直接访问,无需导入
例子
1 | package deploy |
使用Cue制作脚本命令工具
Cue 拥有制作脚本命令工具的功能,它有一个工具层,可用来执行脚本、读写文件以及网络访问等。
规范:
- 脚本文件以
_tool.cue
结尾 - 执行命令为
cue cmd <name>
orcue <name>
例子:
- 脚本文件名为
ex_tool.cue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41package foo
import (
"tool/cli"
"tool/exec"
"tool/file"
)
// moved to the data.cue file to show how we can reference "pure" Cue files
// city: "Amsterdam"
// A command named "prompter"
command: prompter: {
// save transcript to this file
var: file: *"out.txt" | string @tag(file) // you can use "-t flag=filename.txt" to change the output file, see "cue help injection" for more details
// prompt the user for some input
ask: cli.Ask & {
prompt: "What is your name?"
response: string
}
// run an external command, starts after ask
echo: exec.Run & {
// note the reference to ask and city here
cmd: ["echo", "Hello", ask.response + "!", "Have you been to", city + "?"]
stdout: string // capture stdout, don't print to the terminal
}
// append to a file, starts after echo
append: file.Append & {
filename: var.file
contents: echo.stdout // becuase we reference the echo task
}
// also starts after echo, and concurrently with append
print: cli.Print & {
text: echo.stdout // write the output to the terminal since we captured it previously
}
}
- prompter为命令名
- ask/echo/append/print为唯一标识
- cli.Ask/exec.Run/file.Append为函数,
- &{…}为函数参数
- 创建data.cue
1
2
3package foo
city: "Amsterdam" - 运行:
cue cmd prompter
1
2
3
4
5$ cue cmd prompter
What is your name? he
Hello he! Have you been to Amsterdam?
$ cat out.txt
Hello he! Have you been to Amsterdam? - 使用cuelang exec.Run 执行多行代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package foo
import (
"tool/exec"
)
command: {
hello: {
script: #"""
#!/bin/bash
echo hello world
key1="hello you"
ls
echo $key1
"""#
run: cmd: _
run: exec.Run & {
cmd: "bash"
stdin: script
}
}
}
Tips
- A & B === B & A
- A === A
- 路径短写:{a : {b: {c: 5}}} == a: b: c: 5
- 多种类型:a | b | c
- 默认值:number | *1
- 算术: 4 + 5
- 变量引用:”Hello (person)”
- 列表遍历:[ x for x in y ]
- cue 执行 当前目录下的cue文件及父目录下同一个package的cue文件
- cue ./… 以上目录 + 遍历当前目录的子目录下的cue文件
- _开头的变量不会在输出结果中显示,作为局部变量
- [Name=_] 可用来定义一个模板,其中Name匹配任意字段。例如:
1
2
3application: [Name=_]: {
name: string | *Name
} - _|_ 可判断是否存在。例如:if _variable != _|_ { // … }
1
2
3
4
5
6
7a ?: string
if a == _|_ {
b: "a"
}
// 结果为
// cue export a.cue
// b: "a"1
2
3
4
5
6
7
8a: string
if a == _|_ {
b: "a"
}
// 结果为
// cue eval a.cue
// a: string
// b: "a" - 定义映射:map: [string]: string
- 定义切片:slice: […{name:string,value:string}]
实践
- 使用
cue import
将已有的yaml转成Cue
语言1
$ cue import ./... -p kube -l '"\(strings.ToCamel(kind))" "\(metadata.name)"' -fR
- 引入k8s资源的模块
1
2$ go mod init main
$ cue get go k8s.io/api/extensions/v1beta1 -v - 导入k8s资源模块,并创建资源
1
2
3
4
5
6
7package kube
import (
"k8s.io/api/core/v1"
"k8s.io/api/extensions/v1beta1"
)
service <Name>: v1.Service
deployment <Name>: v1beta1.Deployment - cue trim 可用来自动删除冗余字段,以简化配置文件
参考文档
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Michael Blog!
评论