首先先提一个问题,"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