TOML配置文件语言

本文更新于 2019.03.12

TOML的全称是Tom’s Obvious, Minimal Language, 因为它的作者是github联合创始人Tom Preston-Werner. TOML的目标是成为一个有明显语义而容易去阅读的最小化配置文件格式. TOML被设计成可以无歧义地被映射为哈希表, 从而很容易的被解析成各种语言中的数据结构.

示例

# 这是TOML文档示例.

title = "TOML Example"

[owner]
name = "Lance Uppercut"
dob = 1979-05-27T07:32:00-08:00

[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true

[servers]

# 你可以按你的意愿缩进.TOML并不关心你是用Tab还是空格.
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"

[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"

[clients]
data = [ ["gamma", "delta"], [1, 2] ]

# 数组里可以换行
hosts = [
"alpha",
"omega"
]

语法

整体约定

  • TOML是大小写敏感的.
  • TOML文件必须只包含UTF-8编码的Unicode字符.
  • 空格是指制表符(0x09)或空格 (0x20).
  • 换行符是指LF(0x0A)或CRLF (0x0D0A).

注释

用符号#来表示注释:

# I am a comment. Hear me roar. Roar.
key = "value" # Yeah, you can do this.

字符串

有四种方法来表示字符串:基本字符串, 多行基本字符串, 字面量和多行字面量. 所有的字符串必须只包含有效的UTF-8字符.

基本字符串 是由引号括起来的任意字符串, 除了那些必须要转义的, 比如双引号, 反斜杠和控制字符(U+0000到U+001F):

"I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."

常用的转义序列:

\b         - backspace       (U+0008)
\t         - tab             (U+0009)
\n         - linefeed        (U+000A)
\f         - form feed       (U+000C)
\r         - carriage return (U+000D)
\"         - quote           (U+0022)
\\         - backslash       (U+005C)
\uXXXX     - unicode         (U+XXXX)
\UXXXXXXXX - unicode         (U+XXXXXXXX)

任意Unicode字符都可能被转义为uXXXX 或 UXXXXXXXX的形式.这些转义代码必须是有效的 Unicode标量值 .

所有未出现在上面名单中的转义序列必须保留, 如果使用, TOML应该会产生错误.

有时你需要表达一段文本(比如, 翻译文件), 或者是将很长的字符串分成多行.TOML很容易处理这种情况. 多行基本字符串 是被三引号括起来的字符串, 并且允许换行.紧跟起始界定符后面的换行符会被剪掉, 而其他的所有空格和换行字符仍然被保留:

key1 = """
Roses are red
Violets are blue"""

TOML解析器应该能正常处理不同平台下的换行符:

# 对于Unix系统, 上面的多行字符串应该是这样的:
key2 = "Roses are red\nViolets are blue"

# 对于Windows系统, 最可能等价于这样的:
key3 = "Roses are red\r\nViolets are blue"

在行尾使用 \ , 可以避免在写长字符串的时候引入多余的空格. \ 将会删除当前位置到下个非空字符或结束界定符之间的所有空格(包括换行符). 如果在起始界定符之后的第一个字符是反斜杠和一个换行符, 那么从此位置到下个非空白字符或结束界定符之间的所有空格和换行符都会被剪掉. 所有的转义序列对基本字符串都有效, 也对多行基本字符串有效:

# 以下每个字符串都是相同的
key1 = "The quick brown fox jumps over the lazy dog."

key2 = """
The quick brown \


fox jumps over \
    the lazy dog."""

key3 = """\
    The quick brown \
    fox jumps over \
    the lazy dog.\
    """

任何Unicode字符都可能被用到, 除了那些可能需要转义的字符:反斜杠和控制字符(U+0000 到 U+001F). 引号不需要转义, 除非它们的存在可能会造成提前关闭界定符.

如果你需要频繁的指定Windows的路径或正则表达式, 那么不得不添加转义符就会变的繁琐和容易出错. TOML支持完全不允许转义的字面量字符串来帮助你解决此类问题. 字面量字符串 是被单引号包含的字符串, 跟基本字符串一样, 它们一定是以单行出现:

# 所见即所得.
winpath  = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted   = 'Tom "Dubs" Preston-Werner'
regex    = '<\i\c*\s*>'

因为没有转义, 所以在一个被单引号封闭的字面量字符串里面没有办法写单引号. 幸运的是, TOML支持多行版本的字面量字符串来解决这个问题. 多行字面量字符串 是被三个单引号括起来的字符串, 并且允许换行. 跟字面量字符串一样, 这也没有任何转义. 紧跟起始界定符的换行符会被剪掉.界定符之间的所有其他内容都会被按照原样解释而无需修改:

regex2 = '''I [dw]on't need \d{2} apples'''
lines  = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''

对于二进制数据, 建议你使用Base64或其他适合的编码, 比如ASCII或UTF-8编码.具体的处理取决于特定的应用.

整数

整数就是没有小数点的数字.正数前面也可以用加号, 负数需要用负号前缀表示:

+99
42
0
-17

对于大整数, 你可以用下划线提高可读性.每个下划线两边至少包含一个数字:

1_000
5_349_221
1_2_3_4_5     # 有效, 但不建议这样写

前导零是不允许的.也不允许十六进制(Hex), 八进制(octal)和二进制形式. 诸如“无穷”和“非数字”这样的不能用一串数字表示的值都不被允许.

预期的范围是64位 (signed long)(−9,223,372,036,854,775,808 到 9,223,372,036,854,775,807).

浮点数

一个浮点数由整数部分(可能是带有加号或减号前缀的)和小数部分和(或)指数部分组成的数. 如果只有小数部分和指数部分, 那么小数部分必须放在指数部分前面:

# 小数
+1.0
3.1415
-0.01

# 指数
5e+22
1e6
-2E-2

# 小数和指数同时存在
6.626e-34

小数部分是指在小数点后面的一个或多个数字.

指数部分是指E(大写或小写)后面的整数部分(可能用加号或减号为前缀).

和整数类似, 你可以用下划线来提高可读性.每个下划线两边至少包含一个数字:

9_224_617.445_991_228_313
1e1_000

预期精度为64位 (double).

布尔值

布尔值是小写的true和false.

true false

时间日期

时间日期是 RFC 3339 中的时间格式:

1979-05-27T07:32:00Z
1979-05-27T00:32:00-07:00
1979-05-27T00:32:00.999999-07:00

数组

数组是由方括号包括的基本单元.空格会被忽略. 数组中的元素由逗号分隔.数据类型不能混用(所有的字符串均为同一类型):

[ 1, 2, 3 ]
[ "red", "yellow", "green" ]
[ [ 1, 2 ], [3, 4, 5] ]
[ "all", 'strings', """are the same""", '''type'''] # 这样可以
[ [ 1, 2 ], ["a", "b", "c"] ] # 这样可以
[ 1, 2.0 ] # 注: 这样不行

数组也可以多行.所以, 除了忽略空格之外, 数组也忽略了括号之间的换行符. 在结束括号之前存在逗号是可以的:

key = [
1, 2, 3
]

key = [
1,
2, # 这样可以
]

表(也被称为哈希表或字典)是键值对集合.表格名由方括号包裹, 自成一行. 注意和数组相区分, 数组里只有值.

[table] 在表名之下, 直到下一个表或文件尾(EOF)之间都是该表的键值对. 键是等号符左边的值, 值是等号符右边的值. 键名和值周围的空格都将被忽略. 键, 等号和值, 一定要在同一行(有些值可以多行表示)

键可以是裸的或由引号包括的. 裸键 可能仅包含字母, 数字, 下划线和破折号. 引号键 遵循基本字符串的规则, 允许你使用更广泛的键名. 除非有绝对的必要, 否则最好是用裸键.

表中的键值对是无序的:

[table]
key = "value"
bare_key = "value"
bare-key = "value"

"127.0.0.1" = "value"
"character encoding" = "value"
"ʎǝʞ" = "value"

点(.)严禁在裸键中使用, 因为它被用来表示嵌套表! 命名规则为被点分隔的部分应该属于同一个键.(见上文):

[dog."tater.man"]
type = "pug"

等价于如下JSON格式:

{ "dog": { "tater.man": { "type": "pug" } } }

被点分隔部分周围的空格都会被忽略, 但是最好不要使用任何多余的空格:

[a.b.c]          # this is best practice
[ d.e.f ]        # same as [d.e.f]
[ g .  h  . i ]  # same as [g.h.i]
[ j . "ʞ" . l ]  # same as [j."ʞ".l]

如果你不想, 你可以完全不去指定父表(super-tables).TOML知道该如何处理:

# [x] 你
# [x.y] 不
# [x.y.z] 需要这些
[x.y.z.w] # 去处理这种情况

空表是允许的, 其中没有键值对.

只要父表没有被直接定义, 而且没有定义特定的键, 你可以继续写入:

[a.b]
c = 1

[a]
d = 2

你不能多次定义键或表.这样做是无效的:

# 不要这么做
[a]
b = 1

[a]
c = 2

# 也不要这样做
[a]
b = 1

[a.b]
c = 2

所有的表名和键一定不能为空:

# 无效TOML
[]
[a.]
[a..b]
[.b]
[.]
= "no key name" # 不允许

内联表

内联表提供一种更紧凑的语法来表示表.它们可以把数据分组, 避免这些数据很快变得冗长. 内联表是由大括号{ 和 }括起来的.在大括号内可以存在零个或多个逗号分隔的键值对. 内联表里的键值对跟标准表里的键值对形式是一样的.允许所有的值类型, 包括内联表.

内联表一般以单行出现.不允许换行符出现在大括号之间, 除非是包含在值中的有效字符. 即便如此, 也强烈建议不要在把内联表分成多行.如果你有这种需求, 那么你应该去用标准表:

name = { first = "Tom", last = "Preston-Werner" }
point = { x = 1, y = 2 }

上面的内联表完全等同于下面的标准表定义:

[name]
first = "Tom"
last = "Preston-Werner"

[point]
x = 1
y = 2

表数组

最后要介绍的类型是表数组.表数组可以通过包括在双括号内表格名来表达. 使用相同双括号名的每个表都是数组中的元素.表的顺序跟书写顺序一致. 没有键值对的双括号表会被当作空表:

[[products]]
name = "Hammer"
sku = 738594937

[[products]]

[[products]]
name = "Nail"
sku = 284758393
color = "gray"

等价于如下JSON格式:

{
"products": [
    { "name": "Hammer", "sku": 738594937 },
    { },
    { "name": "Nail", "sku": 284758393, "color": "gray" }
]
}

你也能创建内嵌的表数组.只需要对子表使用相同的双括号语法就可以.每个双括号子表将属于其上面最近定义的那个表:

[[fruit]]
  name = "apple"

  [fruit.physical]
    color = "red"
    shape = "round"

  [[fruit.variety]]
    name = "red delicious"

  [[fruit.variety]]
    name = "granny smith"

[[fruit]]
  name = "banana"

  [[fruit.variety]]
    name = "plantain"

上面的TOML对应于下面的JSON格式:

{
"fruit": [
    {
    "name": "apple",
    "physical": {
        "color": "red",
        "shape": "round"
    },
    "variety": [
        { "name": "red delicious" },
        { "name": "granny smith" }
    ]
    },
    {
    "name": "banana",
    "variety": [
        { "name": "plantain" }
    ]
    }
]
}

试图用已经定义的数组的名称来定义的正常表, 在解析的时候一定会抛出错误:

# INVALID TOML DOC
[[fruit]]
  name = "apple"

  [[fruit.variety]]
    name = "red delicious"

  # This table conflicts with the previous table
  [fruit.variety]
    name = "granny smith"

你也可以在适合的地方使用内联表:

points = [ { x = 1, y = 2, z = 3 },
           { x = 7, y = 8, z = 9 },
           { x = 2, y = 4, z = 8 } ]

为什么我要用它呢?

因为我们需要一个像样的人类可读的格式, 同时能无歧义地映射到哈希表. 而且YAML的规范有80页那么长, 真是令人发指! 不, 不考虑JSON .你知道为什么.

In some ways TOML is very similar to JSON: simple, well-specified, and maps easily to ubiquitous data types. JSON is great for serializing data that will mostly be read and written by computer programs. Where TOML differs from JSON is its emphasis on being easy for humans to read and write. Comments are a good example: they serve no purpose when data is being sent from one program to another, but are very helpful in a configuration file that may be edited by hand.

The YAML format is oriented towards configuration files just like TOML. For many purposes, however, YAML is an overly complex solution. TOML aims for simplicity, a goal which is not apparent in the YAML specification: http://www.yaml.org/spec/1.2/spec.html

The INI format is also frequently used for configuration files. The format is not standardized, however, and usually does not handle more than one or two levels of nesting.

目前的实现

这里只给出0.4.0版本的实现:

libtoml试用

libtoml实现了TOML 0.4.0, github: https://github.com/ajwans/libtoml .

和hyperscan一样, libtoml使用了Ragel来解析语法, 因此编译需要依赖Ragel; 另外它使用libicu来处理unicode.

编译

cmake -G “Unix Makefiles” . make

简单示例

#include <toml.h>

struct toml_node *root;
struct toml_node *node;
char *buf = "[foo]\nbar = 1\n";
char *value;

toml_init(&root);
toml_parse(root, buf, len);

node = toml_get(root, "foo.bar");

toml_dump(root, stdout);

value = toml_value_as_string(node);
free(value);

toml_free(root);