jqの出力をパイプでつなげた時の挙動(slurpとunbuffered)について
about jq
jqはjsonの整形に使われる。通常は以下のようなvalidなJSONを入力にする
data.json
{ "name": "foo", "age": 20 }
整ったフォーマットで出力される。
$ cat data.json | jq . { "name": "foo", "age": 20 }
diffなどを取る時には、-S
を使ってキーでソートする。一行だけでみたい場合には-c
でコンパクトな出力にする。
$ cat data.json | jq -S { "age": 20, "name": "foo" } $ cat data.json | jq -c -S {"age":20,"name":"foo"}
invalidな連続したJSON
jqはログ出力がJSONで行われているようなJSONとしてはinvalidな連続したJSONを取ることができる。
function loop(){ for i in `seq $1`; do (>&2 echo $i time) cat <<EOF { "name": "foo", "no": "no.$i" } EOF done } $ loop 5 | jq -S -c loop 5 2>/dev/null | jq . -S -c {"name":"foo","no":"no.1"} {"name":"foo","no":"no.2"} {"name":"foo","no":"no.3"} {"name":"foo","no":"no.4"} {"name":"foo","no":"no.5"}
残念なことにこの出力はJSONではない。
slurp
JSONにしたい場合には --slurp
を使う。
$ loop 5 2>/dev/null | jq . -S --slurp [ { "name": "foo", "no": "no.1" }, { "name": "foo", "no": "no.2" }, { "name": "foo", "no": "no.3" }, { "name": "foo", "no": "no.4" }, { "name": "foo", "no": "no.5" } ]
JSONになったので更にパイプでつなげられる。
$ loop 5 2>/dev/null | jq . -S --slurp | jq 'sort_by(.no) | reverse' [ { "name": "foo", "no": "no.5" }, { "name": "foo", "no": "no.4" }, { "name": "foo", "no": "no.3" }, { "name": "foo", "no": "no.2" }, { "name": "foo", "no": "no.1" } ]
uniqueにしてみたり。
$ loop 5 2>/dev/null | jq . -S --slurp | jq -r 'reverse | unique_by(.name)' [ { "name": "foo", "no": "no.5" } ] $ loop 5 2>/dev/null | jq . -S --slurp | jq 'reverse | unique_by(.name) | .[].no' "foo" $ loop 5 2>/dev/null | jq . -S --slurp | jq 'reverse | unique_by(.name) | .[].no' foo
bashのかたちでループできるようにもできなくもない。
for i in `loop 5 2>/dev/null | jq . -S --slurp | jq -r 'reverse | .[].no'`; do echo "** $i **" done ** no.5 ** ** no.4 ** ** no.3 ** ** no.2 ** ** no.1 **
そして2つの入力をつなげたりもできる。
$ jq -r --slurp 'sort_by(.no) | .[] | .no' <(loop 5 2>/dev/null) <(loop 5 2>/dev/null) no.1 no.1 no.2 no.2 no.3 no.3 no.4 no.4 no.5 no.5
いままで隠していたことがあって、今まで全部標準エラーを隠してきたのだけれど。通常のjqの処理ではバッファリングが行われる。なのでログ出力をフィルタしたりしたい時にその時来た出力をそのまま表示させたいと言う時に困る。特に更にパイプをつなげて後段の処理に整形した結果を渡したいときなどに(後述)
$ loop 5 | jq . -S -c 1 time 2 time {"name":"foo","no":"no.1"} {"name":"foo","no":"no.2"} 3 time {"name":"foo","no":"no.3"} 4 time {"name":"foo","no":"no.4"} 5 time {"name":"foo","no":"no.5"}
もちろん、--slurp
を使えばもとの出力がクローズするまで出力されなくなる。
$ loop 5 | jq . -S -c --slurp 1 time 2 time 3 time 4 time 5 time [{"name":"foo","no":"no.1"},{"name":"foo","no":"no.2"},{"name":"foo","no":"no.3"},{"name":"foo","no":"no.4"},{"name":"foo","no":"no.5"}]
unbuffering
jqではデフォルトでバッファリングされる。なので例えば更に後段にパイプで繋いだときなどに後ろのプロセスに出力がリアルタイムにわたってくれない。
$ loop 5 | jq . -S -c | cat 1 time 2 time 3 time 4 time 5 time {"name":"foo","no":"no.1"} {"name":"foo","no":"no.2"} {"name":"foo","no":"no.3"} {"name":"foo","no":"no.4"} {"name":"foo","no":"no.5"}
バッファリングせずに一個ずつ出力する方法として以下の様な記事があった。
whileとreadを使って毎回プロセスを立ち上げ直す感じ。
$ loop 5 | while read LINE; do echo "${LINE}" | jq "." ; done
最初はなるほどと思ったけれど。これはあまりうまくない。期待する入力の行数が固定されてしまっている。(デフォルトでは1行) 上の `loop` は3行の入力なので失敗する。
実際のところは --unbuffered
のオプションをつければ良い。
$ loop 5 | jq . -S -c --unbuffered | cat 1 time 2 time {"name":"foo","no":"no.1"} {"name":"foo","no":"no.2"} 3 time {"name":"foo","no":"no.3"} 4 time {"name":"foo","no":"no.4"} 5 time {"name":"foo","no":"no.5"}