首先先提一個問題,"abc"
、123
或者 [1, 2, 3]
是否是一個合法的 json ?php
以前一直有在使用一個 json 的命令行工具 jq,這個工具是基於 flex 和 bison 來實現的(去了解這些是基於當年學習 php 的經歷)。後來有段時間我又發現一個不錯的詞法和語法分析工具 antlr,它支持多種語言的生成,而且自己也提供了多種語言的基本語法文件。因此我就想能不用基於它實現一個 go 語言版的 json 命令行工具。git
下面就開始一步一步行動吧(若是想直接看代碼能夠直接拉到底部),我將這個項目命名爲 jtlr
。github
根據我本身常使用的場景,我要實現如下幾個功能:golang
基本用法:json
jtlr '{"a": 1}'
交互模式,能夠屢次輸入,而且最好能支持上下切換:windows
jtlr -a
從標準輸入中讀取內容,能夠格式化實時輸出的日誌:工具
tail -f xxx.log | jtlr -s
從文件中讀取:學習
jtlr -f xxx.log
在動手以前,先要對 json 有一個全面的認識。先來大體看一下官網提供的 json 的 BNF 範式的起始部分:測試
json element value object array string number "true" "false" "null" ... element ws value ws
ws
是 whitespace 的縮寫,即空白字符,忽略這個以後,便可簡單清晰的看到 json 的內的有效數值。雖然咱們經常使用的 json 內容都是 object 起的,但並非必定要從 object 開始,因此對於文章開頭那個問題,你有答案了嗎?flex
在實現時我並無去複製官網提供的 BNF,而是採用了 antlr4 提供的語法,關於它的實現,這裏有一篇文章說明:https://andreabergia.com/a-grammar-for-json-with-antlr-v4/ 。
簡單來講,json 有七種的數據,其中 array
和 object
是能夠再包含 value
,剩下五種就是基本的數值數據。
此外,還有一類比較特殊的狀況,就是對 string
的用法:
member ws string ws ':' element
string
既能夠是一個基本類型的 value
,也能夠一個對象成員的鍵值。這會致使咱們在對 string
作上色等處理時須要考慮着兩種狀況。
使用下面的命令便可生成基於 go 語言的 lexer 和 parser:
antlr -Dlanguage=Go -o parser/ JSON.g4
接下來就是功能實現的工做了。
antlr4 生成的接口比較完備,包含每一個分支邏輯進入、退出和錯誤節點訪問的接口。而且有較好的錯誤糾正和提示機制。
但對於 json 自己這個 case,須要注意的是對 value
和 string
。上面也有提到,全部七類數值都是 value
,因此都會觸發 EnterValue
和 ExitValue
事件,string
同理。
對於 object
和 array
來講,比較棘手的在於嵌套的數據,例如:
{"a": [134, {"a": 1}, true, [1, 2, 3], false]}
在使用 antlr4 提供的接口時,須要標註進入和退出的順序。
最開始我作了個很是簡單的交互模式的實現:
reader := bufio.NewReader(os.Stdin) for { fmt.Print(">>> ") text, err := reader.ReadString('\n') if err == io.EOF { break } if text == "\n" || text == "\r\n" { continue } fn(text) }
可是在這種實現邏輯下,上下左右等按鍵會直接打印在屏幕上而沒法正確處理,由於終端處於 cooked mode 下。go 自己也沒有提供 tty 的封裝。因此要進入 raw mode,一種是經過直接 call 起命令行的方式:
func raw(start bool) error { r := "raw" if !start { r = "-raw" } rawMode := exec.Command("stty", r) rawMode.Stdin = os.Stdin err := rawMode.Run() if err != nil { return err } return rawMode.Wait() }
另一種是操做 stdin 的文件句柄,這樣實現起來就至關複雜了。
出於兼容性和可維護性的考慮,我使用了 golang/crypto 提供的 terminal 的封裝,這也是項目中除了 antlr 之外惟一一個引入的第三方包(若是算是第三方的話)。
可是這個包有一個問題是必須使用 \r\n
進行回車(官方的 issue 解釋是一些歷史緣由吧啦吧啦),否則光標不會回到行首,可是 go 標準的 fmt 包中使用的 \n
換行,而 antlr 使用了 fmt 進行錯誤輸出,因此須要對錯誤輸出進行重載。
從開始構思到實現到當前階段,大概耗時兩個週末了。
因爲前期偷懶,格式化輸出所有使用的是 fmt,這裏後續須要優化一下。
如今的實現對於 antlr 來講有點像用牛刀殺雞,jq 自己支持的節點選取,這是後續實現的一個方向。
另外,go 官方雖然提供了官方的 json 序列化和反序列化工具,可是市面上也有一些第三方的實現被使用,我也想探討一下實現方式。
另外,windows 下還沒作徹底的兼容測試。
最後,貼上項目地址:https://github.com/XiaoLer/jtlr-go