gojqはGoで実装されたjqコマンドおよびライブラリです。2019年の4月から開発を始めて、現在も様々な改善を行っています。
これまでgojqのHomebrew formulaはitchyny/tapより配信していましたが、このたび公式のhomebrew/coreに入りました。 これにより、次のコマンドでインストールできるようになりました。
brew install gojq
tapの考え方についてはTapsを参照してください。
実は、私への連絡もなしにいきなりhomebrew/core
に入れられていて、formulaの品質が悪かったのですが、色々と指摘したらすぐに直してくれました。
自分のコントロールできない部分に置くのが正直嫌だったのですが、すでに入ってしまったのを怒ってもしょうがないし、bottleによるメリットも大きいので良いかなと思います。
ただ、連絡はしてほしかったですね… (というかメイン開発者の意向を無視して勝手にformula追加できるのどうなの…)
formulaの配布元tapを移動するときは、tap_migrations.json
を使います。
tapのリポジトリのトップにこの名前のJSONを作っておけば、既存のtapを使っているユーザーもスムーズにtapの移行が行われます。
今回はhomebrew/core
に移動したので、以下のようなJSONファイルを配置しました。
{ "gojq": "homebrew/core" }
gojqは様々なフィードバックを受けて完成度があがっています。
最近あったものとしては、$HOME
が空のときにうまく動かないというものです (#58)。
デフォルトのモジュールパスの~/.jq
を展開するためにos.UserHomeDir
を使っていたのですが、値を取れないときにエラーとして終了していました。
$HOME
が取れないケースというのは想定したことなかったのです。
AWS Lambdaで動かすとこれが空になるそうですね。
Fuzzingによるコーナーケースの修正も行っています。
例えば ""[:{}]
とか infinite * "x"
のようなクエリのクラッシュが発見されています。
また先日は全探索によるバグの探索も行いました。
コマンド引数に渡すクエリ文字列を生成して、コマンドの実行結果を比較するという感じです。
jqとgojqの終了ステータスの差異をチェックするコード
package main import ( "fmt" "os/exec" "syscall" ) func main() { source := []byte(" ") for { check(string(source)) for i := len(source) - 1; i >= 0; i-- { if source[i] != '~' { source[i]++ if i == 0 && (source[i] == '-' || source[i] == '+') || source[i] == '#' { source[i]++ } if source[i] == '1' { source[i] = ':' } break } source[i] = ' ' if i == 0 { source[0] = '!' source = append(source, '!') } else if i == len(source)-1 { source[i]++ } } } } var sem = make(chan struct{}, 10) func check(src string) { sem <- struct{}{} go func() { defer func() { <-sem }() if x, y := run("jq", "-n", src), run("gojq", "-n", src); x != y { fmt.Printf("ng: %q %d %d\n", src, x, y) } else if x == 0 { fmt.Printf("ok: %q %d %d\n", src, x, y) } }() } func run(command string, args ...string) int { cmd := exec.Command(command, args...) if err := cmd.Start(); err != nil { return -1 } if err := cmd.Wait(); err != nil { if exiterr, ok := err.(*exec.ExitError); ok { if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { return status.ExitStatus() } } } return 0 }
この調査では 1?/1
(lexerのバグで、?//
という演算子をチェックして失敗した後のoffset
のバグ)、0.._
(これもlexerのバグで、クエリのエラーとすべきものを受け付けてしまっていた) や @a?
(compilerのバグで、定義されていないformatをcompile errorとしてはいけない) といったクエリの挙動が異なることが発見されました。
実装に相当自信があっても、Fuzzingや探索を行ってみると、何かしらのバグは見つかるものですね。
gojq v0.12.0からはJSONに色を付けるライブラリを自前で持ち、パフォーマンスが三倍以上改善しました。
以前はgo-prettyprintにお世話になっていましたが、オブジェクトのキーのエスケープがバグっていたのと (#15にて修正済)、NaN
や無限大の正規化が (これらはnull
と出力するというjq
の仕様を実装する必要がある) オーバーヘッドになっていたこと、またこのライブラリのパフォーマンスがあまり良くないことが問題でした。
色付けを行う部分を自前にすることで、jq仕様の正規化も組み込むことができ、またメモリーアロケーションも大幅に減少することができました。
go-prettyjsonのパフォーマンスの改善に関しては#17にてフィードバックしています (二倍近くは速くなりそうです)。
gojqは言語処理系です。 作り始めて二年近くになり、処理系もパーサーもそれぞれ完全なリライトをしています。 コミット数も1000を超え、様々なことを学ぶことができました。 Goのツールにライブラリとしてgojqを埋め込む利用例も増えています。 ソフトウェアは完成することはありません。 これからもgojqを楽しんでいきたいと思います。
gojqは作り始めて一年半経った今でもコードを書く楽しさを教え続けてくれて嬉しい。大体これくらいやってると飽きが来るのだけど、gojqは飽きた頃に新しい話題を持ってきてくれる。
— (っ=﹏=c) .。o○ (@itchyny) 2020年12月21日