notebook

都内でWEB系エンジニアやってます。

jqでファイルに書き出したリストを用いて比較する

調査などである値のリスト出力して他のリストと付け合わて比較したいみたいな場合のメモ書き

  • users.json
[
  {
    "name": "foo1",
    "description": "bbbbb"
  },
  {
    "name": "foo2",
    "description": "aaaaa"
  },
  {
    "name": "bar1",
    "description": "ccccc"
  },
  {
    "name": "bar2",
    "description": "ddddd"
  },
  {
    "name": "baz1",
    "description": "eeeee"
  },
  {
    "name": "baz2",
    "description": "fffff"
  }
]
  • names.txt
foo1
foo2
bar2

users.jsonの中からnames.txtのリストに含まれるかチェックし含まれないものだけ抜き出したい

コマンドラインに貼り付けるなり何なりで直接入力する場合次のような感じで実現できる

$ cat users.json | jq --arg list '["foo1","foo2","bar2"]' '.[]|select([.name]|inside([$list])|not)'
{
  "name": "bar1",
  "description": "ccccc"
}
{
  "name": "baz1",
  "description": "eeeee"
}
{
  "name": "baz2",
  "description": "fffff"
}

サンプル程度の数量であればコマンドラインで直接入力しても問題ないが量が多くなると結構厳しくなってくる

ちょうど1つ1つコマンドラインに入力していくのはだるい量のリストをjq内で使いたい場面があったので調べてみたらすぐ見つかった

いくつかパターンがあったので残しておく

下準備

その前に下準備

names.txtそのままだとjqの-sでも読み込めないため"で囲む

$ cat names.txt | sed -e 's/$/"/g; s/^/"/g'
"foo1"
"foo2"
"bar1"
$ cat names.txt | sed -e 's/$/"/g; s/^/"/g' | jq -sc
["foo1","foo2","bar1"] 
$ cat names.txt | sed -e 's/$/"/g; s/^/"/g' | jq -sc > names.json

細かく試してみると次のような感じ

$ echo 'hoge' | jq
parse error: Invalid numeric literal at line 2, column 0

$ echo '"hoge"' | jq
"hoge"

$ echo '"hoge"' | jq -s
[
  "hoge"
]

sedで愚直にやらずとも次のような生成の方法もあるらしい

$ jq -ncR '[inputs]' <<< $(cat names.txt)
["foo1","foo2","bar1"] 

jqへの値の渡し方いろいろ

調べた中では次の3パターンがあった

  • --argで値を渡す
  • --argjsonで値を渡す
  • --slurpfileで値を渡す

それぞれ見てみる

1. --argで値を渡す

catで展開した値を--argで渡して後のselect内で配列に変換して比較する

$ cat names.json
["foo1","foo2","bar1"]

コマンドライン自体はスッキリしてよい

$ cat users.json| jq --arg list "$(cat names.json)" '.[]|select([.name]|inside([$list])|not)' | jq '.name'
"bar2"
"baz1"
"baz2"

insideの中で[]を使って配列として扱うようにさせている

--arg自体は別にJSON形式の引数を期待しているわけではないので何でも渡せる

渡した時点での解釈は文字列なので配列に変換してあげる必要がある

2. --argjsonで値を渡す

--argjsonだと引数で渡した値を最初からjsonとして解釈してくれる

$ cat users.json| jq --argjson list "$(cat names.json)" '.[]|select([.name]|inside($list)|not)' | jq '.name'
"bar2"
"baz1"
"baz2"

なので1と比較するとinsideの中身が違う

    1. inside([$list])
    1. inside($list)

--slurpfileでファイルから値を渡す

slurpはテキストのリストを配列として扱ってくれるものなので配列のjsonを渡すと二重配列として扱われてしまう

$ cat users.json| jq --slurpfile list names.json '$list'
[
  [
    "foo1",
    "foo2",
    "bar1"
  ]
]

そのためflattenでフラットにしてあげる必要がある

$ cat users.json| jq --slurpfile list names.json '.[]|select([.name]|inside($list|flatten)|not)' | jq '.name'
"bar2"
"baz1"
"baz2"

そういう話でいうと--slurpfileを使うならダブルクオートで囲ったテキストのリストを渡すのが一番素直

まとめ

ファイルから値を読み込んで色々比較するための方法を3つ試してみた

今回の例でいうとinsideで比較する箇所での扱いがそれぞれ微妙に差分が出た

--arg inside([$list])
--argjson inside($list)
--slurpfile inside($list|flatten)

それぞれ特徴があるので使いこなしていきたい

個人的にはこのパターンの使い方だったら--argjsonかなと思った

参考:

How to convert string list to JSON string array in Bash? - Stack Overflow