jq 入門 - シェルで JSON を扱う

jq 入門 - シェルで JSON を扱う

この記事で解決できること

  • curl や API 出力の JSON を シェルから読みやすい形 に整形できる
  • select / map / -r など jq の必須語彙 を最短で身につく
  • パイプ運用で「クオートが付いてシェルに渡せない」「文字化けする」などの 定番事故を防ぐ型 が手に入る

結論(実務の型)

  • とりあえず整形 → jq .
  • 値だけ抜く → jq -r '.field'
  • 配列を 1 行 1 JSON にバラす → jq -c '.[]'
  • 条件で絞る → jq '.[] | select(.status=="ok")'
  • 取得失敗時に止めない → jq '.field // empty'

前提(対象環境)

  • jq 1.6 以降(Ubuntu 20.04 以降の apt install jq で十分。1.7 系の機能差は本文で都度言及)
  • 公式マニュアル: jq Manual

jq とは何か?

jq は JSON 専用のフィルタ言語兼コマンドgrepawk で JSON を無理に切るより、構造を理解した上で値を抜けるため、API 連携・ログ集計・CI スクリプトで事故が減る。

  • 標準入力 → フィルタ → 標準出力という Unix パイプの素直なモデル
  • フィルタ式は 左から右へ 評価される(パイプと同じ感覚)
  • 数値・文字列・配列・オブジェクト・null・boolean を扱える型システムを持つ

jq をどうインストールするか?

最短は OS のパッケージマネージャ。バージョン違いで挙動差があるため、スクリプトに組み込む場合は jq --version で揃えるのが安全。

# Ubuntu / Debian
$ sudo apt update && sudo apt install jq

# RHEL / Rocky / AlmaLinux
$ sudo dnf install jq

# macOS (Homebrew)
$ brew install jq

# バージョン確認
$ jq --version

古い CentOS 7 系では epel-release 経由になる。CI 用に静的バイナリが欲しいときは 公式 Releases の単一ファイルバイナリを /usr/local/bin/jq に置くのが速い。

基本フィルタの読み方は?

jq のフィルタは「入力に対する変換式」。何も書かなければ恒等関数(整形して返す)、.field でオブジェクトから値を取り、.[] で配列を展開する。

1. 整形して表示する

$ echo '{"name":"linny","age":3}' | jq .
{
  "name": "linny",
  "age": 3
}

2. オブジェクトからキーで取り出す

$ echo '{"name":"linny","age":3}' | jq '.name'
"linny"

3. 配列を 1 要素ずつバラす

$ echo '[{"id":1},{"id":2}]' | jq '.[]'
{"id":1}
{"id":2}

シングルクォートで囲むのは シェル変数展開を抑える ため。ダブルクォートを使うと $ が解釈されて事故る。jq の式は常に'...'` で囲うのが鉄則。

オブジェクトと配列をどう扱うか?

.[].field で配列内の各要素から値を抜き、, で複数式を並列に評価、| でフィルタを連結する。grep | awk の感覚そのまま。

配列の各要素から値を抜く

$ echo '[{"id":1,"tag":"a"},{"id":2,"tag":"b"}]' \
  | jq '.[].tag'
"a"
"b"

複数フィールドをタプルで取り出す

$ echo '[{"id":1,"tag":"a"},{"id":2,"tag":"b"}]' \
  | jq '.[] | {id, tag}'
{"id":1,"tag":"a"}
{"id":2,"tag":"b"}

{id, tag}{id: .id, tag: .tag} の省略記法。スネークケースなど予約語以外のキー に限る。

ネストの取り出し(安全演算子 ?

$ echo '{"a":{"b":{"c":42}}}' | jq '.a.b.c'
42

# 存在しないキーは null だが、配列添字エラーは ? で抑制
$ echo '{}' | jq '.a.b?.c?'
null

select でどう絞り込むか?

select(条件)条件が真の値だけを通す フィルタ。配列を .[] で展開してから渡すのが定石。SQL の WHERE 句に近い役割を持つ。

等値で絞る

$ echo '[{"s":"ok"},{"s":"ng"},{"s":"ok"}]' \
  | jq '.[] | select(.s=="ok")'
{"s":"ok"}
{"s":"ok"}

数値比較・複合条件

$ jq '.[] | select(.score >= 80 and .active)'
$ jq '.[] | select(.tag=="a" or .tag=="b")'
$ jq '.[] | select(.tag | startswith("v1"))'

キーの有無で絞る

$ echo '[{"a":1},{"b":2}]' \
  | jq '.[] | select(has("a"))'
{"a":1}

フィルタ後にフィールドを抜く型

jq '.[] | select(.status=="error") | .message'

「絞ってから取り出す」順序を守るとデバッグしやすい。最後に // empty を付けると null を出さずに済む。

map と length と keys で何ができるか?

map(f) は配列の各要素に f を適用し、length は長さ、keys はオブジェクトのキー一覧を返す。配列全体に処理を流す ときに使う。

map:配列を加工する

$ echo '[1,2,3]' | jq 'map(. * 10)'
[
  10,
  20,
  30
]

map(f)[.[] | f] と等価。.[] | f だと 1 要素ずつ流れて くるのに対し、map配列のまま 返るのが違い。

length:要素数を数える

$ echo '[{"id":1},{"id":2},{"id":3}]' | jq 'length'
3

文字列なら文字数、オブジェクトならキー数、null なら 0 を返す。型ごとに意味が変わる点に注意。

keys:キー一覧を取り出す

$ echo '{"a":1,"c":3,"b":2}' | jq 'keys'
[
  "a",
  "b",
  "c"
]

keysソート済みkeys_unsorted は挿入順。再現性が必要なら keys の方が安全。

出力をどう整形するか?(-r / -c)

人間が読む整形は jq .、シェル変数に渡すなら -r(raw 出力)、grep や xargs に流すなら -c(compact)。出力モードの選び方で 8 割の事故が決まる

-r:文字列のクオートを外して生で出す

$ echo '{"name":"linny"}' | jq '.name'
"linny"

$ echo '{"name":"linny"}' | jq -r '.name'
linny

シェルで NAME=$(...) のように受けるなら 必ず -r"linny" を変数に入れると展開時にダブルクォート混入する。

-c:1 行 1 JSON で出す

$ echo '[{"id":1},{"id":2}]' | jq -c '.[]'
{"id":1}
{"id":2}

-c行指向ツールとの連携用while read line ループや xargs -I {} での再投入に向く。

tsv / csv 出力(@tsv / @csv)

$ echo '[{"id":1,"name":"a"},{"id":2,"name":"b"}]' \
  | jq -r '.[] | [.id, .name] | @tsv'
1	a
2	b

@csv は値をクオートし、@tsv はタブ区切り。配列に詰めてから渡す のが正しい使い方。

-r文字列のみ クオートを外す。数値・オブジェクトはそのまま JSON として出る。配列を行に展開したいなら .[] を必ず挟む。

実務でよく使うパターン

API レスポンスの整形、ログの集計、設定ファイル更新の 3 つは現場で頻出。型を覚えておけばコピペで使い回せる

curl で取って必要なフィールドだけ抜く

$ curl -sS "https://api.example.com/users" \
  | jq -r '.users[] | "\(.id)\t\(.name)"'

-sS は静かに(成功時無音、エラー時のみ表示)。"\(.id)\t\(.name)"文字列補間 で自由な区切り文字を作れる。

group_by で集計する

$ jq '[.[] | {status}] | group_by(.status) | map({status: .[0].status, count: length})'

group_by(f)同じ f の結果でグループ化 した配列の配列を返す。map({key: ..., count: length}) でカウント表に整形する。

設定ファイルの一部を書き換える

# package.json の version を更新
$ jq '.version = "1.2.3"' package.json > package.json.tmp \
  && mv package.json.tmp package.json
# sponge を使う安全版
$ jq '.version = "1.2.3"' package.json | sponge package.json

配列に要素を追加する

$ echo '{"items":[1,2]}' | jq '.items += [3]'
{
  "items": [
    1,
    2,
    3
  ]
}

|=再代入オペレータ+=加算代入.items |= map(. * 2) のように深い場所を一括変換できる。

よくある落とし穴は?

null の伝播・型エラー・引数渡しの 3 つで詰まる人が多い。エラーメッセージを読んでから検索する だけで解決時間が半分になる。

1. null をパイプに流して jq: error (at <stdin>:0): Cannot iterate over null

# .users が存在しない場合
$ echo '{}' | jq '.users[]'
jq: error (at <stdin>:1): Cannot iterate over null (null)

対処:// [] でデフォルト値を与える。

$ echo '{}' | jq '.users // [] | .[]'
# 何も出力されない(正常終了)

2. シェル変数を式に埋め込みたい

# NG: クオートが衝突する
$ KEY="name"
$ jq ".$KEY" file.json   # シェル展開後に '."name"' になることがある

# OK: --arg で明示的に渡す
$ jq --arg key "$KEY" '.[$key]' file.json

--arg文字列として--argjsonJSON として 値を渡す。数値や配列を渡したいなら --argjson

3. 文字列が配列っぽく見えるのに .[] できない

$ echo '"abc"' | jq '.[]'
jq: error (at <stdin>:1): Cannot iterate over string ("abc")

文字列は配列ではない。型を確認するには type フィルタ を使う。

$ echo '"abc"' | jq 'type'
"string"

4. 終了コードを判定したい

jq -e出力が falsenull のとき終了コード 1 を返す。条件分岐に組み込める。

$ echo '{"ok":false}' | jq -e '.ok' || echo "失敗"
false
失敗

コピペ用:安全テンプレ

# 整形してページャに流す
jq . file.json | less

# 値を変数に受ける(必ず -r)
NAME=$(curl -sS "$URL" | jq -r '.name')

# 配列を 1 行 JSON にして xargs に流す
curl -sS "$URL" | jq -c '.users[]' \
  | xargs -I {} sh -c 'echo "USER: {}"'

# null 安全に値を抜く
jq -r '.path.to.value // "DEFAULT"'

次に読む