jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

作成日:
読了:約59分
更新日:

当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。

APIのレスポンス、クラウドのCLIの出力、設定ファイル、ログ。開発の現場ではJSONを目にしない日がありません。ところが、1行に詰め込まれた数千文字のJSONから「statusがpaidの注文の金額だけ合計したい」と思ったとき、grepやsedでは手に負えなくなります。

そこで使うのがjqです。jqはJSONを読み込み、短い式(フィルタ)で整形・抽出・変換して出力するコマンドです。jq .で見やすく整形するだけでも便利ですが、selectやmap、group_byまで覚えると、ちょっとした集計スクリプトをPythonで書く必要がなくなります。

この記事では、jqの考え方から、よく使うフィルタ、オプション、ファイルを書き換えるときの注意、NDJSONや巨大なファイルの扱い、終了ステータスとよくあるハマりどころまでを、実行結果付きで説明します。

NOTE

2026年11月11日に、jq 1.8 Manual、jqのダウンロードページ、GitHubのjqlang/jqのリリースノートで仕様と変更点を確認しました。コマンドの実行結果は、特に断りがなければmacOS 27.2、Homebrewのjq 1.8.1、bash 5.3.9のものです。確認時点の最新リリースはjq 1.8.2(2026年6月20日公開)で、本文の機能はすべて1.8.1で動作を確かめています。サンプルのJSONはすべて自作のダミーデータです。

先に結論: よく使う書き方の早見表

まず、使う頻度が高いものを並べます。式の意味は後の章で順に説明します。

やりたいこと書き方
整形して表示するjq . file.json
1行に詰めて出すjq -c . file.json
文字列をクォートなしで出すjq -r '.name' file.json
入れ子のフィールドを取るjq '.a.b.c' file.json
配列の先頭を取るjq '.[0]' file.json
配列の要素を1つずつ出すjq '.[]' file.json
オブジェクトのキー一覧jq 'keys' file.json
要素数jq 'length' file.json
シェル変数を文字列で渡すjq --arg name "$NAME" ...
シェル変数を数値やJSONで渡すjq --argjson n 30 ...
複数のJSONを1つの配列にまとめるjq -s . a.json b.json
入力なしでJSONを組み立てるjq -n '...'

パイプ|を使う式は表に入れにくいので、コードでまとめておきます。

パイプを使うよく使う式
# 条件に合う要素だけ残す
jq '.users[] | select(.active)' users.json
 
# 配列の各要素を変換する
jq '.users | map(.name)' users.json
 
# 必要なフィールドだけのオブジェクトにする
jq '.users[] | {name, role}' users.json
 
# CSVにする(-r が必要)
jq -r '.users[] | [.id, .name] | @csv' users.json
 

覚えておくと事故を防げることも3つ挙げておきます。

  • jq ... file.json > file.jsonはファイルを空にします。一時ファイルに書いてからmvします。
  • --argで渡した値は必ず文字列です。数値として比べたいときは--argjsonを使います。
  • -eを付けないと、結果がnullでもjqは終了ステータス0で終わります。

jqとは: 入力、フィルタ、出力

jqは、JSONを入力として受け取り、フィルタを適用して、結果をJSONとして出力するプログラムです。

jqの基本の流れ
JSONの入力(0個以上)  ->  フィルタ  ->  JSONの出力(0個以上)

ここで大事なのは、入力も出力も「1個」とは限らない点です。

  • 入力ファイルに{"a":1}と{"a":2}が2つ並んでいれば、フィルタは2回実行されます。
  • .[]のようなフィルタは、1つの入力から複数の値を出力します。
  • selectは条件に合わないと何も出力しません(出力0個)。

jqの式は、この「値の流れ(ストリーム)」を左から右へ加工していく書き方です。シェルのパイプでコマンドをつなぐのと似ていて、jqの中でも|で処理をつなぎます。シェルのパイプそのものの仕組みはパイプとリダイレクトの記事で説明しています。

もう1つの前提は、.(ドット)が「今の入力」を表すことです。.だけのフィルタは入力をそのまま出力するので、jq .は整形して表示するだけ、という意味になります。.nameは「今の入力のnameフィールド」です。パイプの右側では、.は左側の出力に置き換わります。この「.が指すものが場所によって変わる」感覚が、jqを読み書きするときの中心になります。

インストールとバージョン

インストール

公式のダウンロードページには、主な環境向けに次のコマンドが載っています。

主な環境でのインストール
# macOS(Homebrew)
brew install jq
 
# Debian / Ubuntu
sudo apt-get install jq
 
# Windows(winget)
winget install jqlang.jq
 
# Docker(インストールせずに試す)
docker run -i --rm ghcr.io/jqlang/jq -n 'range(3)'

GitHubのリリースページには、Linux、macOS、Windows向けの単体バイナリもあります。ブラウザで試すだけなら、公式のプレイグラウンドplay.jqlang.orgが使えます。

バージョンはjq --versionで確認します。

バージョンの確認
jq --version
/usr/bin/jq --version
実行結果
jq-1.8.1
jq-1.7.1-apple

手元のmacOS 27.2には、Homebrewとは別に/usr/bin/jq(1.7.1)も入っていました。PATHの順番によってはこちらが使われ、trimや@uridのような1.8で追加された関数がnot definedのエラーになります。手元のmacOS 27.2には、Homebrewとは別に/usr/bin/jq(1.7.1)も入っていました。PATHの順番によってはこちらが使われ、1.8で追加された関数が使えません。

マニュアルの関数が動かないときは、まずwhich jqとjq --versionでどのjqを使っているかを確認してください。Linuxのディストリビューションのパッケージも、最新版より古いことがあります。

バージョンごとの主な変更

jqは1.6(2018年11月)から1.7(2023年9月)まで、5年近くリリースが空きました。1.7のリリースノートでは、リポジトリがjqlangのOrganizationへ移ったことが告知されています。リリースノートから、この記事の内容に関係する変更を抜き出します。

バージョン公開日主な変更
1.72023-09-06リポジトリがjqlangへ移動。pick、abs、--raw-output0を追加。elseのないifを許可。数値リテラルの精度を保つように。NO_COLORに対応。--argfileを削除
1.7.12023-12-131.7の修正版
1.8.02025-06-01番号を1.X.Y形式に。trim、ltrim、rtrim、trimstr、toboolean、@urid、add/1、skip/2を追加。エラーメッセージにフィルタ内の位置を表示。ホームページをjqlang.orgへ移動
1.8.12025-07-01セキュリティ修正。1.8.0で入れたreduceとforeachの変更を性能低下のため取り消し
1.8.22026-06-20多数のCVEの修正。Windows arm64版を追加。@uriと@uridのマルチバイト文字の不具合を修正

1.8.0には互換性に関わる変更もあります。indexやindicesが文字列のバイト位置ではなく文字(コードポイント)の位置を返すようになり、tonumberは前後に空白がある文字列をエラーにするようになりました。古いスクリプトを1.8で動かすときは、1.8.0のリリースノートの該当箇所に目を通しておくと安心です。1.8.2はセキュリティ修正が多いので、信頼できない入力を扱う環境では更新しておくのがよいでしょう。

サンプルデータ

以降の例では、次の2つのファイルを使います。users.jsonはチームのメンバー一覧、orders.ndjsonは1行に1つのJSONが並ぶ注文ログです。

users.json
{
  "team": "sample-dev",
  "updated": "2026-11-01",
  "users": [
    {"id": 1, "name": "Alice", "role": "admin", "age": 34, "tags": ["go", "infra"], "email": "alice@example.com", "active": true},
    {"id": 2, "name": "Bob", "role": "member", "age": 28, "tags": ["js"], "email": null, "active": false},
    {"id": 3, "name": "Carol", "role": "member", "age": 41, "tags": ["python", "js"], "email": "carol@example.com", "active": true},
    {"id": 4, "name": "Dave", "role": "guest", "age": 23, "tags": [], "active": true}
  ]
}
orders.ndjson
{"order_id":"A-001","user_id":1,"amount":1200,"status":"paid"}
{"order_id":"A-002","user_id":3,"amount":800,"status":"paid"}
{"order_id":"A-003","user_id":1,"amount":300,"status":"refunded"}
{"order_id":"A-004","user_id":2,"amount":4500,"status":"paid"}

Bobはemailがnull、Daveはemailのキー自体がなく、tagsも空です。この違いが後で効いてきます。

整形と出力の形

.で整形、-cで1行に

.は入力をそのまま返すフィルタです。jqは出力するときに整形するので、結果として読みやすいJSONになります。

整形と圧縮
jq . users.json | head -5
jq -c . users.json | cut -c1-80
実行結果
{
  "team": "sample-dev",
  "updated": "2026-11-01",
  "users": [
    {
{"team":"sample-dev","updated":"2026-11-01","users":[{"id":1,"name":"Alice","rol

-c(--compact-output)は1つのJSONを1行で出します。ログに書き出したり、次のコマンドへ1行ずつ渡したりするときに使います。-S(--sort-keys)を付けると、オブジェクトのキーをアルファベット順に並べ替えます。差分を取る前に順番をそろえたいときに便利です。

-rで文字列のクォートを外す

文字列を出力すると、jqはJSONとして正しい形、つまりダブルクォート付きで出します。シェルの変数に入れたり、別のコマンドに渡したりするときは、-r(--raw-output)でクォートを外します。

-r の有無
jq '.team' users.json
jq -r '.team' users.json
実行結果
"sample-dev"
sample-dev

-rが効くのは文字列だけです。数値やオブジェクトはそのままJSONで出ます。

フィールドと配列の取り出し

.a.bでフィールドをたどる

.nameでフィールドを取り出し、.a.bのようにつなげると入れ子をたどれます。

フィールドの取り出し
jq '.users[0].name' users.json
jq '.nothing' users.json
jq '.nothing.deeper' users.json
実行結果
"Alice"
null
null

存在しないキーを指定してもエラーにはならず、nullが返ります。nullに対してさらに.deeperを取ってもnullです。この性質のおかげで深い場所を気軽に参照できる一方、キー名のタイプミスに気付きにくい原因にもなります。

キー名にハイフンや空白が含まれるときは、."x-y"か.["x-y"]の形で書きます。.x-yと書くと「.xからyを引く」と解釈され、エラーになります。

型が違うとエラーになる

キーがないのは問題ありませんが、文字列や数値に対してフィールドを取ろうとするとエラーです。

文字列からフィールドを取ろうとする
jq '.team.name' users.json
実行結果
jq: error (at users.json:10): Cannot index string with string "name"

?を付けると、このエラーを握りつぶして何も出力しません。

? でエラーを無視する
jq '.team.name?' users.json
echo "exit=$?"
実行結果
exit=0

?はtryの短縮形です。後で説明するtry ... catchと同じく、データの形がそろっていない入力を扱うときに使います。ただし、何でも?を付けると本当の異常まで見えなくなるので、付ける場所は絞ります。

.[0]、スライス、.[]

配列は.[0]のように番号で取り出します。負の数は末尾からの位置で、.[1:3]のように書くとスライス(1番目から3番目の手前まで)になります。

配列の要素とスライス
jq '.users[-1].name' users.json
jq '.users[1:3] | length' users.json
実行結果
"Dave"
2

.[]は配列の要素を1つずつ別々の値として出力します。ここがjqで一番大事な記号です。

.[] で要素を展開する
jq '.users[].name' users.json
jq -r '.users[].name' users.json
実行結果
"Alice"
"Bob"
"Carol"
"Dave"
Alice
Bob
Carol
Dave

.users[].nameは「users配列の各要素について.nameを取る」という意味で、結果は配列ではなく4つの文字列です。オブジェクトに.[]を使うと、値が1つずつ出てきます。

配列やオブジェクトでないものに.[]を使うとエラーになります。.[]?にするとエラーの代わりに何も出しません。

文字列に .[] を使う
jq '.team[]' users.json
実行結果
jq: error (at users.json:10): Cannot iterate over string ("sample-dev")

パイプ|とカンマ,

パイプで処理をつなぐ

A | Bは「Aの出力それぞれを入力にしてBを実行する」です。Aが4つの値を出せば、Bは4回実行されます。

パイプ
jq '.users[] | .name' users.json

これは.users[].nameと同じ結果になります。短い式では.a.b、処理を段階に分けたいときは|、と使い分けます。

カンマで複数の値を出す

A, Bは「Aの出力の後にBの出力を続ける」です。1つの入力から複数のものを取り出したいときに使います。

カンマで複数の値を出す
jq '.users[] | .name, .age' users.json | head -4
実行結果
"Alice"
34
"Bob"
28

|はカンマより優先順位が低い点に注意します。.users[] | .name, .ageは.users[] | (.name, .age)と解釈されます。迷ったら括弧を付けるのが確実です。

配列とオブジェクトを組み立てる

[...]で配列にまとめる

[ 式 ]は、式が出力した値をすべて集めて1つの配列にします。.[]でばらした値を、もう一度配列に戻すときの書き方です。

配列にまとめる
jq -c '[.users[].name]' users.json
実行結果
["Alice","Bob","Carol","Dave"]

{...}でオブジェクトを作る

オブジェクトは{キー: 式}の形で作ります。キーと同じ名前のフィールドを取り出すだけなら、{name, role}のように省略できます。

オブジェクトを組み立てる
jq -c '.users[] | {name, role}' users.json
jq -c '.users[] | {user: .name, tag_count: (.tags | length)}' users.json
実行結果
{"name":"Alice","role":"admin"}
{"name":"Bob","role":"member"}
{"name":"Carol","role":"member"}
{"name":"Dave","role":"guest"}
{"user":"Alice","tag_count":2}
{"user":"Bob","tag_count":1}
{"user":"Carol","tag_count":2}
{"user":"Dave","tag_count":0}

値の部分にパイプを書くときは、(.tags | length)のように括弧で囲みます。キーを式で決めたいときは、キー側を括弧で囲みます。

キーを式で決める
jq -c '{(.team): (.users | length)}' users.json
実行結果
{"sample-dev":4}

selectで絞り込む

条件に合う値だけを通す

select(条件)は、条件が真なら入力をそのまま出し、偽なら何も出しません。.[]と組み合わせて、配列から要素を絞り込むのが定番です。

select の基本
jq -r '.users[] | select(.age >= 30) | .name' users.json
jq -r '.users[] | select(.role == "member" and .active) | .name' users.json
実行結果
Alice
Carol
Carol

比較は==、!=、<、<=、>、>=、論理演算はand、or、notです。notは関数なので、.active | notのようにパイプで渡して使います。

いろいろな条件
# 正規表現にマッチするもの
jq -r '.users[] | select(.name | test("^[AC]")) | .name' users.json
 
# 候補のどれかに一致するもの
jq -c '[.users[] | select(.role | IN("admin", "guest")) | .name]' users.json
実行結果
Alice
Carol
["Alice","Dave"]

testの正規表現はOnigurumaというライブラリで処理されます。testの正規表現はOniguruma(PCREに近い書き方)です。正規表現の書き方そのものは正規表現の実践ガイドを参照してください。

jqで偽になるのはfalseとnullだけ

jqでは、偽として扱われるのはfalseとnullの2つだけです。0や空文字列、空配列は真になります。JavaScriptやPythonの感覚で書くと間違えるところです。

真偽の判定
jq -n -c '0, "", [], {}, null, false | if . then "truthy" else "falsy" end'
実行結果
"truthy"
"truthy"
"truthy"
"truthy"
"falsy"
"falsy"

たとえばselect(.tags | index("js"))は、"js"が配列の0番目にあるとindexが0を返します。0は真なので正しく残りますが、「見つからなければnull」という仕様に頼った書き方です。意図が読み取りやすいanyやINを使うほうが安全です。

絞り込んだ件数を数える

selectの結果は配列ではないので、件数を数えるときは[...]で囲んでからlengthを使います。

件数を数える
jq '[.users[] | select(.active)] | length' users.json
実行結果
3

mapと集計

mapで配列を変換する

map(f)は、配列の各要素にfを適用した新しい配列を返します。中身は[.[] | f]と同じです。

map の基本
jq -c '.users | map(.age)' users.json
jq -c '.users | map(select(.active) | .name)' users.json
実行結果
[34,28,41,23]
["Alice","Carol","Dave"]

mapの中でselectを使うと、条件に合わない要素が消えた配列になります。.[] | select(...)がばらばらの値を出すのに対して、map(select(...))は配列のまま返るので、結果をそのまま次の配列処理に渡せます。

add、length、min、max

数値の配列はaddで合計できます。平均はadd / lengthです。

合計と平均と最小最大
jq '.users | map(.age) | add' users.json
jq '.users | map(.age) | add / length' users.json
jq -c '.users | map(.age) | min, max' users.json
実行結果
126
31.5
23
41

addは文字列の配列なら連結、配列の配列なら結合、オブジェクトの配列なら合体します。

オブジェクトを調べる: keys、has、length、type

keysはオブジェクトのキーをアルファベット順に並べた配列を返します。元の順番が欲しいときはkeys_unsortedです。

keys と keys_unsorted
jq -c '.users[0] | keys' users.json
jq -c '.users[0] | keys_unsorted' users.json
実行結果
["active","age","email","id","name","role","tags"]
["id","name","role","age","tags","email","active"]

lengthは対象によって意味が変わり、配列なら要素数、オブジェクトならキーの数、文字列なら文字数です。

has("キー")はキーが存在するかを返します。値がnullでもキーがあればtrueです。

has と値の違い
jq -c '.users[] | [.name, has("email"), .email]' users.json
実行結果
["Alice",true,"alice@example.com"]
["Bob",true,null]
["Carol",true,"carol@example.com"]
["Dave",false,null]

Bob("email": null)とDave(キーなし)は、.emailではどちらもnullになり区別できません。区別が必要な処理ではhasを使います。値の型はtypeで調べられます。

to_entriesとwith_entries

キーと値のペアに分解する

to_entriesは、オブジェクトを{"key": キー, "value": 値}の配列に変換します。from_entriesはその逆です。キーを値として扱いたいとき、つまり「キー名で絞り込む」「キー名を書き換える」ときに使います。

to_entries と from_entries
jq -n -c '{"a": 1, "b": 2} | to_entries'
jq -n -c '[{"key": "x", "value": 1}, {"key": "y", "value": 2}] | from_entries'
実行結果
[{"key":"a","value":1},{"key":"b","value":2}]
{"x":1,"y":2}

with_entriesでまとめて変換する

with_entries(f)はto_entries | map(f) | from_entriesの短縮形です。キーと値のペアを1つずつfで加工して、オブジェクトに戻します。

with_entries の例
# 値が文字列のフィールドだけ残す
jq -c '.users[0] | with_entries(select(.value | type == "string"))' users.json
 
# キー名を小文字にそろえる
jq -n -c '{"Content-Type": "json", "X-Debug": "1"} | with_entries(.key |= ascii_downcase)'
実行結果
{"name":"Alice","role":"admin","email":"alice@example.com"}
{"content-type":"json","x-debug":"1"}

並べ替えとグループ化: sort_by、group_by、unique_by

sort_by

sort_by(式)は、式の値で配列を並べ替えます。降順はreverseを続けるか、数値なら符号を反転させます。式をカンマで並べると、複数のキーで並べ替えます。

sort_by
jq -c '.users | sort_by(.age) | map(.name)' users.json
jq -c '.users | sort_by(-.age) | map(.name)' users.json
jq -c '.users | sort_by(.role, .name) | map([.role, .name])' users.json
実行結果
["Dave","Bob","Alice","Carol"]
["Carol","Alice","Bob","Dave"]
[["admin","Alice"],["guest","Dave"],["member","Bob"],["member","Carol"]]

型が混ざった値を並べると、jqはnull、false、true、数値、文字列、配列、オブジェクトの順に並べます。

型をまたいだ並び順
jq -n -c '[{}, [], "a", 1, true, false, null] | sort'
実行結果
[null,false,true,1,"a",[],{}]

group_byで集計する

group_by(式)は、式の値が同じ要素ごとに配列をまとめ、配列の配列を返します。グループは式の値の順に並びます。集計はたいていgroup_byの後にmapを続けます。

ロールごとの人数と名前
jq -c '.users | group_by(.role) | map({role: .[0].role, count: length})' users.json
jq -c '.users | group_by(.role) | map({key: .[0].role, value: map(.name)}) | from_entries' users.json
実行結果
[{"role":"admin","count":1},{"role":"guest","count":1},{"role":"member","count":2}]
{"admin":["Alice"],"guest":["Dave"],"member":["Bob","Carol"]}

SQLのGROUP BY roleとCOUNT(*)に当たる処理です。.[0].roleは「グループの先頭要素のrole」で、同じグループ内ではどれも同じ値なので先頭を使います。

uniqueとunique_by

uniqueは重複を取り除いて並べ替えた配列を、unique_by(式)は式の値ごとに最初の1件を残した配列を返します。

unique と unique_by
jq -c '[.users[].tags[]] | unique' users.json
jq -c '.users | unique_by(.role) | map(.name)' users.json
実行結果
["go","infra","js","python"]
["Alice","Dave","Bob"]

変数と外部からの値: --arg、--argjson、$ENV

シェル変数は--argで渡す

シェルの変数をjqの式に埋め込むとき、式の文字列に直接$nameを埋め込むのは避けます。--arg 名前 値で渡すと、jqの中で$名前として使えます。

--arg で値を渡す
jq -r --arg role member '.users[] | select(.role == $role) | .name' users.json
実行結果
Bob
Carol

--argの値は、クォートや改行を含んでいても安全に文字列として扱われます。

--argの値は必ず文字列

--argで渡した値は常に文字列です。数値と比べると、エラーにならないまま結果が空になります。

--arg で数値を渡すと比較が効かない
jq -r --arg min 30 '.users[] | select(.age >= $min) | .name' users.json
echo "---"
jq -r --argjson min 30 '.users[] | select(.age >= $min) | .name' users.json
実行結果
---
Alice
Carol

前の章の並び順のとおり、jqでは数値は常に文字列より小さいので、34 >= "30"は偽になります。数値やJSONとして渡したいときは--argjsonを使うか、($min | tonumber)で変換します。

環境変数と$ARGS

環境変数は$ENV.名前かenv.名前で読めます。マニュアルによると、$ENVはjqが起動した時点の環境変数、envはjqの現在の環境です。

環境変数を読む
TARGET=Carol jq -r '.users[] | select(.name == $ENV.TARGET) | .id' users.json
実行結果
3

名前付きの引数は$ARGS.namedに、--argsの後ろに並べた値は$ARGS.positionalに入ります。ファイルの中身を変数に入れる--slurpfile(JSONを配列として)と--rawfile(テキストのまま)もあります。1.6まであった--argfileは1.7で削除されたので、古い記事のコマンドを使うときは--slurpfileに置き換えます。

as $xと、.が変わる落とし穴

式の途中の値は式 as $名前 | ...で変数に入れられます。これが必要になるのは、selectなどの内側では.が別のものを指すためです。

ユーザーごとに注文の合計を出す例で確かめます。最初の書き方は間違いです。

内側の . は外側の . ではない
jq -c --slurpfile orders orders.ndjson '.users[] | {name, n: ([$orders[] | select(.user_id == .id)] | length)}' users.json | head -1
実行結果
{"name":"Alice","n":0}

select(.user_id == .id)の中の.は注文のオブジェクトなので、.idは注文のid(存在しないのでnull)です。ユーザーのidを使うには、外側で変数に入れておきます。

as $id で外側の値を保存する
jq -c --slurpfile orders orders.ndjson '.users[] | .id as $id | {name, total: ([$orders[] | select(.user_id == $id) | .amount] | add // 0)}' users.json
実行結果
{"name":"Alice","total":1500}
{"name":"Bob","total":4500}
{"name":"Carol","total":800}
{"name":"Dave","total":0}

addは空配列に対してnullを返すので、// 0で0にしています。//は後の章で説明します。

主なオプション

よく使うオプションを整理します。

オプション長い名前働き
-r--raw-output文字列をクォートなしで出す
-j--join-output-rに加えて改行を付けない
-c--compact-output1つの結果を1行で出す
-s--slurp入力をすべて読み、1つの配列にしてから実行する
-n--null-input入力を読まず、nullを入力として1回実行する
-e--exit-status最後の出力によって終了ステータスを変える
-R--raw-input入力をJSONではなく、1行ずつの文字列として読む
-S--sort-keysキーを並べ替えて出す
(なし)--arg / --argjson変数を渡す
(なし)--stream入力をパスと値の組に分解して読む

-sと-n

-sは入力をすべて読んでから1つの配列にまとめます。複数のJSONをまとめて集計したいときに使います。

-s でまとめて集計する
jq -s 'map(select(.status == "paid") | .amount) | add' orders.ndjson
実行結果
6500

-nは入力を読まずに式を1回だけ実行します。計算や、JSONを組み立てるだけの用途で使います。

-n で入力なしに実行する
jq -n '1 + 2'
jq -n -c '{created: (now | todate | length)}'
実行結果
3
{"created":20}

-nとinput、inputsを組み合わせると、入力を自分のペースで読めます。inputsは残りの入力をすべて順に出す関数です。-sと違って配列を作らずに1件ずつ処理できるので、件数が多いときは-nとinputsのほうがメモリを使いません。

inputs で1件ずつ読む
jq -n '[inputs | .amount] | add' orders.ndjson
実行結果
6800

-R(--raw-input)を付けると、各行をJSONではなく文字列として読みます。

フォーマット文字列と文字列補間

@csv、@tsv

@csvと@tsvは配列をCSVやTSVの1行に変換します。結果は文字列なので、-rを付けて出します。

CSV と TSV に変換する
jq -r '(["id", "name", "role"] | @csv), (.users[] | [.id, .name, .role] | @csv)' users.json
jq -r '.users[] | [.id, .name, .role] | @tsv' users.json
実行結果
"id","name","role"
1,"Alice","admin"
2,"Bob","member"
3,"Carol","member"
4,"Dave","guest"
1	Alice	admin
2	Bob	member
3	Carol	member
4	Dave	guest

@csvは文字列をダブルクォートで囲み、中のダブルクォートを2つ重ねてエスケープします。配列の中にオブジェクトや配列があるとエラーになるので、先にスカラー値だけの配列にしておきます。

@base64、@uri、@html、@sh

ほかにも、文字列をエンコードするフォーマットがあります。

各種エンコード
jq -r -n '"user:pass" | @base64'
jq -r -n '"dXNlcjpwYXNz" | @base64d'
jq -r -n '"東京 a&b=c" | @uri'
jq -r -n '"%E6%9D%B1%E4%BA%AC" | @urid'
jq -r -n '[1, "a b"] | @sh'
実行結果
dXNlcjpwYXNz
user:pass
%E6%9D%B1%E4%BA%AC%20a%26b%3Dc
東京
1 'a b'

@base64dはBase64のデコード、@urid(1.8.0以降)はパーセントエンコーディングのデコードです。@shは値をシェルで安全に使える形にクォートします。Base64とパーセントエンコーディングの仕組みは、Base64の記事とURLエンコードの記事で説明しています。JWTのペイロードを覗くときにも@base64dが使えますが、JWTはBase64URLなので記号の置き換えが必要です。詳しくはJWTの記事を参照してください。

\(...)で文字列に値を埋め込む

文字列の中に\(式)を書くと、式の結果が埋め込まれます。

文字列補間
jq -r '.users[] | "\(.name) (\(.age)) <\(.email // "no-email")>"' users.json
実行結果
Alice (34) <alice@example.com>
Bob (28) <no-email>
Carol (41) <carol@example.com>
Dave (23) <no-email>

フォーマットと補間を組み合わせて@uri "...\(...)"と書くと、埋め込む部分だけがエンコードされます。URLのクエリを作るときは、値をエンコードしてから埋め込みます。

値だけエンコードしてURLに埋め込む
jq -r -n --arg q 'jq 使い方' '"https://example.com/search?q=\($q | @uri)"'
実行結果
https://example.com/search?q=jq%20%E4%BD%BF%E3%81%84%E6%96%B9

代替演算子//

a // bは、aがfalseかnull(か何も出さない)ときにbを返します。既定値の設定に使います。

// で既定値を入れる
jq -r '.users[] | .email // "(none)"' users.json
実行結果
alice@example.com
(none)
carol@example.com
(none)

注意点は、falseも置き換えの対象になることです。0や空文字列は真なので置き換わりません。

// が置き換える値
jq -n -c 'false // "default", (0 // "default"), ([] | .[0] // "empty")'
実行結果
"default"
0
"empty"

.active // trueのように書くと、activeがfalseのユーザーまでtrueになります。真偽値のフィールドに既定値を入れるときは、if has("active") then .active else true endのように書き分けます。.email // emptyと書けば、値がないものを出力から取り除けます。

値を書き換える: del、=、|=

delでフィールドを消す

del(パス)は指定したパスを取り除きます。カンマで複数指定でき、selectと組み合わせて配列の要素も消せます。

del の例
jq -c '.users[0] | del(.email, .tags)' users.json
jq -c 'del(.users[] | select(.active | not)) | .users | map(.name)' users.json
実行結果
{"id":1,"name":"Alice","role":"admin","age":34,"active":true}
["Alice","Carol","Dave"]

=と|=の違い

jqの代入は、入力全体を受け取って「一部を書き換えた全体」を返します。元のファイルが書き換わるわけではありません。

  • パス = 値: 右辺は元の入力全体を.として評価される
  • パス |= 式: 右辺はそのパスの今の値を.として評価される
|= は今の値を変換する
jq -c '.users[].name |= ascii_upcase | [.users[].name]' users.json
jq -c '.users[0] | .age += 1 | {name, age}' users.json
実行結果
["ALICE","BOB","CAROL","DAVE"]
{"name":"Alice","age":35}

+=、-=、*=なども|=の仲間で、今の値に対して計算します。=と|=の違いがはっきり出る例が次です。

= と |= の右辺の . の違い
jq -c '.users[0] | .name = .role | {name, role}' users.json
jq -c '.users[0] | .name |= .role' users.json
実行結果
{"name":"admin","role":"admin"}
jq: error (at users.json:10): Cannot index string with string "role"

=では右辺の.roleがユーザーのオブジェクトから取られます。|=では右辺の.が.nameの値(文字列"Alice")になるので、文字列から.roleを取ろうとしてエラーになります。

パスの左辺にselectを書くと、条件に合う場所だけを書き換えられます。存在しないパスに代入すると、途中のオブジェクトも作られます。

条件付きの代入とパスの作成
jq -c '(.users[] | select(.role == "guest") | .role) = "member" | [.users[].role]' users.json
jq -c '.meta.version = "2" | .meta' users.json
実行結果
["admin","member","member","member"]
{"version":"2"}

reduceで畳み込む

reduce 値の列 as $x (初期値; 更新式)は、値を1つずつ取り出して、状態を更新していきます。更新式の中の.は今の状態です。

reduce で合計と集計
jq -n 'reduce inputs as $o (0; . + $o.amount)' orders.ndjson
jq -c -n 'reduce inputs as $o ({}; .[$o.status] += 1)' orders.ndjson
jq -c '.users | reduce .[] as $u ({}; .[$u.role] += [$u.name])' users.json
実行結果
6800
{"paid":3,"refunded":1}
{"admin":["Alice"],"member":["Bob","Carol"],"guest":["Dave"]}

2つ目の例は、ステータスごとの件数を数えています。.[$o.status]がまだない(null)ときも、null + 1は1になるので初期化が要りません。group_byは配列全体を読んで並べ替えるのに対して、reduceとinputsの組み合わせは1件ずつ処理するので、件数が多いログの集計に向いています。

ファイルを書き換えるときの注意

jqにはファイルをその場で書き換えるオプションがありません。そこで次のように書きたくなりますが、これはファイルを空にします。

やってはいけない例(bash)
wc -c work.json
jq '.team = "x"' work.json > work.json
echo "exit=$?"
wc -c work.json
実行結果
     529 work.json
exit=0
       0 work.json

シェルはjqを起動する前に、> work.jsonの処理としてwork.jsonを空にして開きます。jqが読みに行ったときには中身がなく、空の入力に対して何も出力しないまま正常終了します。リダイレクトの順序の詳細はパイプとリダイレクトの記事で扱っています。zshでnoclobberが有効な場合は、file existsのエラーで上書き自体が止まります。

安全な書き方は、一時ファイルに書いてから置き換える方法です。&&でつなぐと、jqが失敗したときに元のファイルが残ります。

一時ファイル経由で書き換える
tmp=$(mktemp) && jq '.team = "sample-ops"' work.json > "$tmp" && mv "$tmp" work.json
jq -r .team work.json
実行結果
sample-ops

moreutilsのspongeを使う方法もあります。spongeは標準入力をすべて読み終えてからファイルに書き込むので、jq '...' work.json | sponge work.jsonと書けます。ただし、パイプの前段のjqが失敗して何も出力しなかった場合にspongeがどう振る舞うかは、この記事では確かめていません。失敗時に中身が消えないことを手元で確認してから使ってください。ただし、パイプの途中でjqが失敗してもspongeは空の入力をそのまま書き込むので、失敗時に中身が消えないかは確認してから使ってください(set -o pipefailだけでは防げません)。

curlと組み合わせる

APIのレスポンスから必要なところだけ取る

jqが一番活躍するのは、HTTP APIのレスポンスを読むときです。GitHubのREST APIで、jq自身のリポジトリの情報を取ってみます。

GitHub API のレスポンスから一部を取り出す
curl -sS https://api.github.com/repos/jqlang/jq | jq '{name: .full_name, license: .license.spdx_id, default_branch, html_url}'
実行結果
{
  "name": "jqlang/jq",
  "license": "NOASSERTION",
  "default_branch": "master",
  "html_url": "https://github.com/jqlang/jq"
}

リリースの一覧から、正式版のタグと公開日をTSVで出すこともできます。

リリース一覧をTSVにする
curl -sS 'https://api.github.com/repos/jqlang/jq/releases?per_page=5' | jq -r '.[] | select(.prerelease | not) | [.tag_name, (.published_at | .[0:10])] | @tsv'
実行結果
jq-1.8.2	2026-06-20
jq-1.8.1	2025-07-01
jq-1.8.0	2025-06-01
jq-1.7.1	2023-12-13
jq-1.7	2023-09-06

この結果は2026年11月11日時点のもので、新しいリリースが出れば変わります。認証なしのリクエストは回数に制限があり、確認時のレスポンスヘッダーのx-ratelimit-limitは60でした。APIの設計の考え方はREST APIの設計の記事、エラー時のステータスコードはHTTPステータスコードの記事で扱っています。

失敗を見逃さない

curlとjqをパイプでつなぐと、curlの失敗が見えにくくなります。存在しないリポジトリを指定すると、GitHubは{"message": "Not Found", ...}のようなエラーのJSONを返すので、jqはそれを普通に処理してしまいます。curlに-f(--fail)を付けるとHTTPのエラーでcurlが失敗し、jqには何も渡りません。jqは空の入力では何も出さずに終了ステータス0で終わるので、パイプライン全体としては成功に見えます。スクリプトではset -o pipefailを使い、さらに値が取れなかったことをjq -eで検出します。pipefailについてはset -euo pipefailの記事で詳しく説明しています。

スクリプトでの書き方の例
set -euo pipefail
tag=$(curl -fsS https://api.github.com/repos/jqlang/jq/releases/latest | jq -er '.tag_name')
echo "latest: $tag"
実行結果
latest: jq-1.8.2

NDJSON(JSON Lines)を扱う

1行に1つのJSONを並べた形式は、JSON LinesやNDJSONと呼ばれ、ログやデータの書き出しでよく使われます。jqは入力に複数のJSONが並んでいれば順に処理するので、NDJSONはそのまま読めます。

NDJSON を1行ずつ処理する
jq -c 'select(.amount > 1000)' orders.ndjson
実行結果
{"order_id":"A-001","user_id":1,"amount":1200,"status":"paid"}
{"order_id":"A-004","user_id":2,"amount":4500,"status":"paid"}

全体を集計するときは、-sで配列にするか、-nとinputsまたはreduceを使います。

ユーザーごとの合計金額
jq -s -c 'group_by(.user_id) | map({user_id: .[0].user_id, total: (map(.amount) | add)})' orders.ndjson
実行結果
[{"user_id":1,"total":1500},{"user_id":2,"total":4500},{"user_id":3,"total":800}]

逆に、配列をNDJSONにしたいときは.[]と-cを組み合わせます(jq -c '.users[]' users.json)。

途中に壊れた行が混ざっていると、jqはそこでパースエラー(終了ステータス5)になって止まります。壊れた行を飛ばしたいときは、-Rで行を文字列として読み、fromjson?で1行ずつ解釈します。

壊れた行を目印付きで残す
printf '{"a":1}\n{bad}\n{"a":3}\n' | jq -c -R 'fromjson? // {"error": .}'
実行結果
{"a":1}
{"error":"{bad}"}
{"a":3}

大きなファイルと--stream

jqは通常、1つのJSONをすべてメモリに読み込んでから処理します。数GBの1つの配列のように、1つのJSONが巨大な場合は、--streamでパスと値の組に分解しながら読む方法があります。

--stream の出力
echo '{"a":{"b":[10,20]},"c":"x"}' | jq -c --stream '.'
実行結果
[["a","b",0],10]
[["a","b",1],20]
[["a","b",1]]
[["a","b"]]
[["c"],"x"]
[["c"]]

[パス, 値]の組が葉の値、[パス]だけのものは「ここで配列やオブジェクトが閉じた」という印です。fromstreamでこれを組み立て直し、truncate_streamでパスの先頭を削ると、巨大な配列の要素を1つずつ取り出せます。

items 配列の要素を1つずつ取り出す
jq -n -c '{items: [range(5) | {id: ., name: "item-\(.)"}]}' > big.json
jq -c -n --stream 'fromstream(2 | truncate_stream(inputs))' big.json | head -2
実行結果
{"id":0,"name":"item-0"}
{"id":1,"name":"item-1"}

2 | truncate_stream(...)は、パスの先頭2つ("items"と配列の番号)を削る指定です。トップレベルが配列のファイルなら1にします。--streamは書き方が難しいので、メモリに収まらない場合の手段と考えるのがよいでしょう。--streamは書き方が難しく、全体を読む方式より遅くなることもあるので、メモリに収まらない場合の手段と考えるのがよいでしょう。データを作る側を選べるなら、最初からNDJSONで書き出すほうが扱いやすくなります。

エラーと終了ステータス

終了ステータスの一覧

マニュアルとリリースノート、手元の実行結果から、jqの終了ステータスを整理します。

終了ステータス意味
0正常に実行できた(-eの場合は、最後の出力がfalseでもnullでもない)
1-eのとき、最後の出力がfalseかnullだった
2使い方の誤りやシステムエラー(ファイルが開けないなど)
3フィルタの文法エラー(コンパイルエラー)
4-eのとき、一度も有効な結果が出なかった
5実行時エラーや入力のパースエラー(halt_errorの既定値も5)

それぞれ実際に確かめます。

終了ステータスの確認
jq -e '.users[] | select(.name == "Zed")' users.json; echo "exit=$?"
jq -e '.nothing' users.json; echo "exit=$?"
jq '.users[' users.json; echo "exit=$?"
jq . missing.json; echo "exit=$?"
echo '{"a":1,}' | jq .; echo "exit=$?"
実行結果
exit=4
null
exit=1
jq: error: syntax error, unexpected end of file at <top-level>, line 1, column 7:
    .users[
          ^
jq: 1 compile error
exit=3
jq: error: Could not open file missing.json: No such file or directory
exit=2
jq: parse error: Expected another key-value pair at line 1, column 8
exit=5

1.8.0から、文法エラーのメッセージにフィルタ内の位置が^で示されるようになりました。

-eでシェルの条件に使う

-eを付けないと、結果がnullでも終了ステータスは0です。「値があるかどうか」でシェルの処理を分けたいときは、if jq -e '...' file.json > /dev/null; thenのように-eを付けます。

try ... catchとerror

try 式 catch 式で実行時エラーを捕まえられます。catchの側では、.がエラーメッセージになります。error("メッセージ")で自分でエラーを起こすこともできます。

try と catch
jq -n -c 'try error("boom") catch "caught: \(.)"'
jq -c '[.users[] | try (.email | ascii_downcase) catch "N/A"]' users.json
実行結果
"caught: boom"
["alice@example.com","N/A","carol@example.com","N/A"]

よくあるハマりどころ

フィルタはシングルクォートで囲む

jqのフィルタには$や"、|、!のようにシェルで特別な意味を持つ記号がよく出てくるので、フィルタ全体をシングルクォートで囲むのが基本です。ダブルクォートで囲むと、シェルが先に中身を解釈します。

ダブルクォートで囲んだ場合(bash)
jq -n -c "$ENV.HOME"
jq -r ".users[] | select(.name == "Alice") | .id" users.json
実行結果
null
jq: error: Alice/0 is not defined at <top-level>, line 1, column 28:
    .users[] | select(.name == Alice) | .id
                               ^^^^^
jq: 1 compile error

1つ目はシェルが$ENVを(空の)シェル変数として展開し、jqには.HOMEだけが渡っています。2つ目はシェルのクォートが途中で閉じて、"Alice"のダブルクォートが消えています。シェル変数を式の文字列に埋め込むのはやめて、--argで渡してください。値にダブルクォートが含まれると、式の文法が壊れたり、意図しない式が実行されたりします。

nullと「キーがない」と「空配列」

.emailは値がnullでもキーがなくてもnullです。また、空配列の.[0]もエラーではなくnullになります。

空配列の .[0]
jq -c '[.users[] | .tags[0]]' users.json
実行結果
["go","js","python",null]

nullに対して文字列の関数(ascii_downcaseなど)を使うとエラーになります。データの形がそろっていないときは、// emptyで除くか、//で既定値を入れるか、hasで分けるかを先に決めておきます。

数値の精度

マニュアルによると、jqの数値は内部ではIEEE 754の倍精度浮動小数点数で表されます。1.7からは、入力のリテラルを変更しなければ元の表記のまま出力するようになりましたが、計算すると倍精度に丸められます。

大きな整数と小数
echo '{"id": 12345678901234567890, "price": 0.1}' > nums.json
jq -c '.id' nums.json
jq -c '.id + 0' nums.json
jq -c '.price + 0.2' nums.json
jq -n -c '9007199254740993, 9007199254740993 + 0'
実行結果
12345678901234567890
12345678901234567000
0.30000000000000004
9007199254740993
9007199254740992

そのまま出すだけなら桁が保たれますが、+ 0をした途端に値が変わっています。2^53(9007199254740992)を超える整数は、倍精度では正確に表せません。64ビット整数のIDのように大きな値を扱うときはTwitterのIDのような大きな整数のIDを扱うときは、計算や比較をせずに出力するか、JSON側で文字列にしておくのが安全です。0.1 + 0.2が0.30000000000000004になる理由は浮動小数点数の記事で説明しています。

代替ツール

jqと同じような用途のツールもあります。確認できたものを簡潔に紹介します。

ツール説明(GitHubのリポジトリの説明文から)URL
gojqGoで書かれたjqの実装github.com/itchyny/gojq
jaq正確さ、速さ、簡潔さを重視したjqのクローン(Rust製)github.com/01mf02/jaq
yq(mikefarah版)YAML、JSON、XML、CSV、TOMLなどを扱うGo製のプロセッサgithub.com/mikefarah/yq
fxターミナルで使うJSONビューアー兼プロセッサgithub.com/antonmedv/fx

yqにはgojqの作者itchyny氏は、jq本体のリリースノートにも多くの変更の担当者として名前が出てくるメンテナーです。yqには、jqを内部で呼び出してYAMLを扱うPython製のkislyuk/yqという同名の別ツールもあり、文法が違うので注意が必要です。これらのツールはjqと完全に互換ではありません。細かい関数の有無や数値の扱いはそれぞれのドキュメントで確認してください。この記事ではjq以外のツールの動作は確かめていません。

まとめ

  • jqは「入力 -> フィルタ -> 出力」のプログラムで、値は0個以上のストリームとして流れる。.は今の入力を指す
  • .a.bでフィールド、.[0]で要素、.[]で要素をばらして出す。[...]で配列に、{...}でオブジェクトに組み立て直す
  • 絞り込みはselect、変換はmap、キーの操作はto_entriesとwith_entries、集計はgroup_byとreduce
  • 偽になるのはfalseとnullだけ。//はfalseも置き換える
  • シェルの値は--arg(文字列)と--argjson(JSON)で渡す。フィルタはシングルクォートで囲む
  • jq ... f > fはファイルを空にする。一時ファイルに書いてからmvする
  • スクリプトでは-eとset -o pipefailで失敗を検出する。終了ステータスは0〜5で意味が分かれる
  • 2^53を超える整数は計算すると精度が落ちる。大きなIDは文字列で扱う

まずはjq .での整形から始め、.[]とselect、mapの3つを使えるようになると、APIのレスポンスやログの調査がかなり楽になります。式を試すときは、play.jqlang.orgやjq -nで小さく確かめながら組み立てていくのがおすすめです。

Linux パーミッションの仕組み - chmod・chown・umask と Permission denied の調べ方

Linux パーミッションの仕組み - chmod・chown・umask と Permission denied の調べ方

約50分

ls -lのrwxの読み方と755・644・600・700の使い分けから、カーネルが所有者・グループ・その他のどのビットを見るかの判定順序、ファイルとディレクトリでrwxの意味が違う理由、chmodの数値表記と記号表記、chown、umaskのマスク計算、setuid・setgid・sticky bitまでを実行結果付きで説明します。パーミッションが正しく見えるのにPermission deniedになるときの、親ディレクトリ・ACL・noexec・immutable属性・SELinuxの切り分け手順も整理します。

パイプとリダイレクトの仕組み - 2>&1の意味をファイルディスクリプタから理解する

パイプとリダイレクトの仕組み - 2>&1の意味をファイルディスクリプタから理解する

約46分

2>&1は「標準エラー出力を、今の標準出力の行き先に複製する」という意味です。ファイルディスクリプタとopen file descriptionの関係、シェルがforkとexecの間でopenとdup2を行う流れ、> file 2>&1と2>&1 > fileの結果が違う理由を段階ごとの表で説明します。パイプの容量やSIGPIPE、PIPESTATUSとpipefail、stdioのバッファリングで出力順が入れ替わる現象、sudo echoやsort file > fileの失敗、bashとzshとPOSIXの違いまで、実行結果付きで整理します。

crontabの書き方入門 - cronの5つのフィールドと、動かないときに見る実行環境・ログ・二重起動

crontabの書き方入門 - cronの5つのフィールドと、動かないときに見る実行環境・ログ・二重起動

約26分

crontabは「分 時 日 月 曜日 コマンド」の1行で、決まった時刻にコマンドを動かす設定ファイルです。5つのフィールドと特殊文字(* , - /)の読み方、日と曜日を両方指定したときのOR条件、@dailyなどの短縮表記、crontab -eと/etc/cron.dの違い、PATHや%の落とし穴、タイムゾーン、ログの見方、flockによる二重起動の防止、systemd timerやGitHub Actionsのscheduleとの違いを、crontab(5)などのmanページを一次ソースに、cron式を試せるデモ付きで整理します。

楽天市場のおすすめPR / 楽天アフィリエイト