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"}