有关 tom-toml 的一些事儿

为何要再写一个TOML解析器

  • 学习写解析器
  • 支持注释
  • 支持格式化输出并保持次序

学习写解析器

一直认为编写解析器是很是有挑战性的任务. TOML 自己已经很简洁. 为 TOML 写个解析器颇有吸引力. 咱们知道已经有了 YACC 这样工具能够完成此类工做. 事实上 Go 提供了这样的工具, TOML 上也有关于 EBNF 的讨论, 已经出现出几个版本. 可是要让这些 EBNF 定义转换成特定语言的代码, 还有不少辅助工做. 做为学习目的, 我采起先手工写一个解析器, 能够对完整使用 EBNF 有更深入的理解.git

鉴于 TOML 的简洁. 手工写出全部的 First 集和 Follow 集是可行的. parser.go 中 stateEmpty/tokensEmpty 就是 First 集, 按照编译原理所阐述的, 解析完整结束也会回到 First 集. 解析开始的时候至少要匹配到 First 集合中的一项(TOML 没有二义性, 只匹配一个). 解析结束的时候会回到 First 集合, 因为我写的 First 集合中没有 EOF 匹配, 因此当匹配不了 First 集合时, 解析结束, 相反若是在 First 集合中写下 stateEof/tokensEof 那最终会以匹配 EOF 而结束. 其余的 Follow 集合也是必需要有匹配, 若是没有被匹配, 那表示输入无效, 实现中我在每一个 stateXX 中增长了一个没有匹配到要执行的动做, 用来给出一点提示信息.github

扫描器

解析器是有明确的阶段, 其中词法分析(也能够称为扫描器)是第一阶段. 对于手工写的解析器, 这些阶段的代码能够混合在一块儿. tom-toml 的扫描器 Scanner 是一个纯粹的 UTF-8 字符扫描器, 每次只扫描一个 UTF-8 字符, 别致的地方在于 Scanner 消除了 token 匹配中常见的 peek 操做. First 集和 Follow 集具体的匹配代码写法和这种 Scanner 是配合的, 因此在 itsString 这样的 token 匹配代码中能够看到 flag 这个状态标志, peek 被消除了. 固然这种方法只是一种尝试, 我并不肯定是否能够广泛适用. 采用这种写法有个缘由维护 peek 总让我晕头转向.数组

支持注释

TOML 的实现有不少, 在 tom-toml 以前, 不少实现都是不支持注释操做的, 我认为注释是必要被支持的. 曾经 fork 了 pelletier/go-toml 并增长了注释支持, 好像 pelletier 不理解支持注释是必要. 鉴于改造的比较大, 不如从新写一个解析器.数据结构

兼容性

先写下解释用的 TOML 文本app

[nameOftable] # Kind() 为 TableName, String() 同此行
key1 = "v1" # Kind() 为 String, String() 是 "v1"
key2 = "v2" # Kind() 为 String, String() 是 "v2"
[[arrayOftables]] # Kind() 为 ArrayOfTables, String() 是此行及如下行
key3 = "v3" # Kind() 为 String, String() 是 "v3"

由于采用 map 和支持注释的缘由, 使用上有些特别. Toml 对象中存储的工具

  • TableName 仅是 TOML 规范中的 [nameOftable] 的字面值.
  • Table 仅是 TOML 规范中的 [[arrayOftables]] 的一个 Table.

所以用 tm 表示上述 Toml 对象的话学习

tm["nameOftable"] 仅仅是 `[nameOftable]`, 不包含 Key/Value 部分
tm["arrayOftables"] 是所有的 `arrayOftables`, 由于它是数组
tm.Fetch("nameOftable") 是`[nameOftable]`的 Key/Value 部分, 类型是 Toml
tm["arrayOftables"].Table(0) 是第一个 Table, 类型也是 Table
tm["nameOftable.key1"] 直接访问到了值为 "v1" 的数据

能够看出ui

  • 只有经过 Fetch() 方法才能获得一个 TOML 规范中定义的 Table 的主体.
  • 只有经过 Table() 方法才能获得 Table 类型.
  • arrayOftables.key3 这种写法是错误的, 不知足 TOML 规范的定义

看上去很古怪, 可是若是要用 map 进行存储的话只能是这样, 就算不支持注释, 也逃不过 ArrayOfTables 的古怪.设计

map 带来 "nameOftable.key1" 这种点字符串方便的同时也产生了一些反作用.code

map 更多的是表现平板式的数据结构, 没有太深的嵌套. 你能够用

<!-- lang: cpp -->
tm["a.b.c.d.foo"]  // 一下就访问到最终的目标
// 而不用像这样
tm.Get("a").Get("b").Get("c").Get("d").Get("foo")

TOML v0.2.0 定义中是能够深层嵌套的. 用 map 彻底实现 TOML 的标准, 访问的时候必然产生一些语义上的差别.

Value 和 Item

因为上述的特别缘由, tom-toml 在实现中, 把 TOML 定义中的段(Table/ArrayOfTables)和值(String, Integer ...)分开进行定义. 事实上 Table 的存储也被 Value 负责, 在 tom-toml 中 TableName 实际上就是个空的 Value. 所以会有这样的判断代码

<!-- lang: cpp -->
func (p *Value) IsValid() bool {
    return p.kind != InvalidKind && (p.v != nil || p.kind == TableName )
}

保留这个空的 Table 对 Toml 对象格式化输出TOML文本是有意义的.

Value 的方法 Int/String/Float/Boolean/Datetime 是仿照 reflect.Value 的方法设计的. 也就是说使用者要本身肯定 Value 的 Kind 并调用相应的方法获取数据的值, 若是错误的调用(String方法特殊, 其余类型能够转换到 string), 方法不会产生错误, 会返回一个缺省值.

Item 扩展自 Value, 目前是为了支持 ArrayOfTables 的, 能够看出 Value 主要负责存储值的维护, Item 维护了复杂的类型定义.

支持格式化输出并保持次序

通过解析获得 Toml 对象后, 能够进行增删改全部 TOML 所支持的元素, 包括注释. 操做完后能够用 TomlString/String 方法获得带缩进的格式化输出. Toml 使用 map 保存数据, go 语言中 map 是无序的, tom-toml 内部使用一个计数器保证输出次序.

ArrayOfTables

这个名字很很差, 由于事实上通过分析, 这个定义就是容许以数组的形式进行 TOML 嵌套. 下面转贴官方在 讨论 中给出的例子, 这明明就是嵌套的 TOML.

[[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"

贡献

若是您有任何问题, 建议请 issues 反馈.

相关文章
相关标签/搜索