CONL:用于配置文件的 "Markdown" 格式
CONL:用于配置文件的 "Markdown" 格式
Conrad Irwin — 2025 年 4 月 在我的日常软件开发工作中,我需要不断地与复杂的配置文件打交道。截至 2025 年,它们通常采用以下三种格式之一:JSON-with-comments、YAML 或 TOML。
尽管这些格式被广泛使用,但在实践中使用它们却很痛苦:
- JSON-with-comments —— 在 JSON 文件中注释掉一行非常困难,因为你最终会在前一行留下一个多余的
,
,从而破坏语法。 - TOML —— 为什么
[]
和[[]]
做的是不同的事情,但看起来却一样?还有a.b = true
与a = {"b" = true}
呢?TOML 中的 M 更像是 “muddled(混乱)” 而不是 “minimal(最小)”。 (另见). - YAML —— 63 种不同的多行字符串格式?tags??,norway problem (如果你的 YAML 解析器不再支持 YAML v1.1,这个问题就解决了……)。
我还见过一些新的格式试图添加类型、for 循环和其他“只是稍微编程一下”的功能;但我认为这完全是倒退。如果你需要生成一个配置文件,请使用你最喜欢的图灵完备的编程语言。
目标是创建一个最小的格式,感觉像是配置文件的 “markdown”。它应该是:
- 易于阅读和编辑
- 表示一个类似 JSON 的数据模型
- 易于实现
我对它非常满意!
; a conl document defines key value pairs
; values are usually scalars
port = 8080
; but can also be lists of values
watch_directories
= ~/go
= ~/rust
; or maps of keys to values
env
REGION = us-east1
QUEUE_NAME = example-queue
; multiline scalars work like markdown
; with an optional syntax hint
init_script = """bash
#!/usr/bin/env bash
echo "hello world!"
CONL 的格式现在可以认为是“稳定的”,所以我鼓励你开始使用它!
我已经有了 Rust 和 Go 的工作实现;一个 language server 和一个 Zed 扩展。
CONL 的极小功能集使你可以轻松地构建自己的实现(如果你喜欢这种风格,你甚至可以一次性完成整个过程)。如果你想将其移植到你喜欢的语言,这里有一个更正式的 spec 和一个可重用的 test suite,其中包含示例 CONL 文档及其等效的 JSON。
设计决策(供好奇者参考……)
考虑到我想要创建一个易于阅读 且 易于解析的东西,很明显它需要一个最小的、明确的语法。
我深受 INI critique of TOML 的启发,以避免语法类型的陷阱。乍一看,"false"
和 false
是相同的值(尽管第一个在 JavaScript 中是 true),因此语法不应区分这两者。
另一方面,当你想在文档中构建结构时,INI 确实会失败(TOML 在某种程度上也是如此)。在实践中,软件配置通常是一个嵌套树,JSON 和 YAML 可以很好地处理;所以我想保留这种能力。
这导致了一个数据模型,其中每个值都是 scalar|list|map
之一(与 JSON 的 null|bool|number|string|object|array
相比,这感觉很好)。我也尝试过允许 rust 风格的 enums,并提供语法来选择 enum 变体;但尽管它在配置文件中是一个常用的功能,但编程语言对它的支持并不好,所以它被取消了。
简化模型的首要限制是不支持往返。你可以将任何 JSON 文档转换为 CONL,或者将任何 CONL 文档转换为 JSON;但是 JSON -> CONL -> JSON 会丢失类型信息。
当时 JSON-with-comments 也让我特别恼火。配置文件中最常见的操作是“注释掉我刚刚添加的内容,看看是否能解决问题”。JSON-with-comments 的问题是它也有逗号;你需要确保它们存在于正确的位置。这很快让我产生了每个键都应该在其自己的行上定义的想法。换行符是极好的分隔符。不再需要管理逗号了!
结构
考虑到每个键都应该有自己的行,我最初的嵌套 maps/lists 方法是使用 JSON 分隔符。这可行,但感觉有点牵强(这也让我真的很想添加逗号来获得像 TOML 中的行字面量)。
a = {
b = 2
c = [
1
2
我从使用 TOML 的经验中知道,我不想要同一事物的两种不同格式(特别是如果一个版本使用换行符作为分隔符,而另一个版本使用逗号)。我也知道,如果你需要做比顶级更复杂的事情,表语法(尽管最初很酷)会让你注意到缺乏结构。
我还注意到,如果你查看第一个参数,实际上是明确的。如果 =
在开头,你有一个列表,否则你有一个 map。这确实使实现者感到困惑,但避免了你将 [
和 {
混淆并收到令人困惑的错误消息的可能性。
我过去/现在都很担心缩进。也就是说,如果它对 Python 来说足够好,我认为它对我来说也足够好……
最后一个边界情况是没有值的键。我想将这些设为一个错误:
a ; error?!
b = c
但是,然后我意识到,如果我注释掉一个 map 的内容,那将成为一个错误。所以现在,如果你想表示一个没有值的键(或一个 JSON null
),你可以这样做。
语法
CONL 的第一个版本使用 #
作为注释标记,但我很快遇到了问题。URLs 包含 #
,所以我的下一个版本要求 #
前面有一个空格。然后我注意到了颜色,它们 以 #
开头(我想修复 a =#
不是 注释的解析问题)。另一个常见的注释是 //
,但在 URLs 中使用了它,所以我必须保留我的间距规则。答案最终来自 INI。;
很少用在配置文件中找到的值中;所以它成为了注释字符。
我从很早以前就知道我想要 markdown 的多行字符串。所以这意味着 """
必须是一个保留标记。也就是说,我最初非常反对带引号的标量。我想避免有多种方式来表示同一事物。所以相反,我决定使用 "
作为转义字符(有点像 Powershell 使用反引号):
"= = "; ; {"=":";"}
在使用了它一段时间后,问题在于(在已经有些罕见的需要转义的情况下)你经常需要多个转义。嵌入一个 Regex 非常痛苦,因为你通常不会寻找 = 和 ; 作为 “你需要转义的东西”。
因此,为了使 CONL 更贴近主流,我切换到带有反斜杠转义的双引号字面量。为了避免 JSON 的 UCS-2 问题,我不希望支持 \u0000
(并且 C 的 \U0000000
似乎不必要地冗长,因为 unicode 的范围为 10FFFF)。因此,对于代码点,使用可变长度的 \{0000}
。
; keys and values can contain anything
; except ; (and = for keys).
welcome message = Whatcha "%n"!
; types are deferred until parse time;
; the app knows what it wants.
enabled = yes
country_code = no
; units are encouraged for numeric values
timeout = 500ms
; if you need an empty string or
; escape sequences, use quotes.
empty_string = ""
indent = "\t"
; the following escape sequences
; work inside quoted literals
escape_sequences
= "\\" ; '\'
= "\"" ; '"'
= "\t" ; tab
= "\n" ; newline
= "\r" ; carriage return
= "\{1F321}" ; 🐱 (or any codepoint)
再见,感谢阅读! ← Tools for Go modules
我是 Bluesky 上的 @ConradIrwin (和 X), github:ConradIrwin 或 me@cirw.in。