有关 tom-toml 的一些事儿

前端之家收集整理的这篇文章主要介绍了有关 tom-toml 的一些事儿前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

为什么要再写一个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

猜你在找的Go相关文章