一直認爲編寫解析器是很是有挑戰性的任務. 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 對象中存儲的工具
[nameOftable]
的字面值.[[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 的標準, 訪問的時候必然產生一些語義上的差別.
因爲上述的特別緣由, 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 內部使用一個計數器保證輸出次序.
這個名字很很差, 由於事實上通過分析, 這個定義就是容許以數組的形式進行 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 反饋.