为什么要再写一个TOML解析器
学习写解析器
一直认为编写解析器是非常有挑战性的任务. TOML 本身已经很简洁. 为 TOML 写个解析器很有吸引力. 我们知道已经有了 YACC 这样工具可以完成此类工作. 事实上 Go 提供了这样的工具,TOML 上也有关于 EBNF 的讨论,已经出现出几个版本. 但是要让这些 EBNF 定义转换成特定语言的代码,还有很多辅助工作. 作为学习目的,我采取先手工写一个解析器,可以对完整使用 EBNF 有更深刻的理解.
鉴于 TOML 的简洁. 手工写出所有的 First 集和 Follow 集是可行的. parser.go 中 stateEmpty/tokensEmpty 就是 First 集,按照编译原理所阐述的,解析完整结束也会回到 First 集. 解析开始的时候至少要匹配到 First 集合中的一项(TOML 没有二义性,只匹配一个). 解析结束的时候会回到 First 集合,由于我写的 First 集合中没有 EOF 匹配,所以当匹配不了 First 集合时,解析结束,相反如果在 First 集合中写下 stateEof/tokenSEOf 那最终会以匹配 EOF 而结束. 其他的 Follow 集合也是必须要有匹配,如果没有被匹配,那表示输入无效,实现中我在每个 stateXX 中增加了一个没有匹配到要执行的动作,用来给出一点提示信息.
扫描器
解析器是有明确的阶段,其中词法分析(也可以称为扫描器)是第一阶段. 对于手工写的解析器,这些阶段的代码可以混合在一起. 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 文本
[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" 的数据
可以看出
- 只有通过
Fetch()
方法才能得到一个 TOML 规范中定义的 Table 的主体. - 只有通过
Table()
方法才能得到Table
类型. arrayOftables.key3
这种写法是错误的,不满足 TOML 规范的定义
看上去很古怪,但是如果要用 map 进行存储的话只能是这样,就算不支持注释,也逃不过 ArrayOfTables 的古怪.
map 带来 "nameOftable.key1" 这种点字符串方便的同时也产生了一些副作用.
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 反馈.
原文链接:https://www.f2er.com/go/191120.html