jq . data.json把压缩的 JSON 缩进展开、上色,先看清结构再写路径。
约 3209 字大约 11 分钟
2026-05-14
API 返回、配置文件、容器元数据、结构化日志……现在到处都是 JSON。用 grep 去抠 JSON 是新手最常见的弯路——JSON 的换行和缩进不固定,按行匹配根本不可靠。jq 是专门理解 JSON 结构的工具,按“字段路径”取值,还能筛选、转换、统计和重新组织数据,常被称为“命令行里的 JSON 瑞士军刀”。
用 jq 在命令行里格式化、按路径提取、筛选和改写 JSON:. 取字段、[] 展开数组、-r 输出裸字符串、select 过滤、map 转换,以及和 curl、tail、Shell 脚本的组合用法。
jq . data.json把压缩的 JSON 缩进展开、上色,先看清结构再写路径。
jq '.user.name' data.json用点号沿对象层级取值,路径就是字段名。
jq '.items[].id' data.json[] 把数组每个元素摊开,再取每个元素的字段。
jq -r '.token' data.json-r 去掉外层引号,结果可直接喂给别的命令。
jq '.items[] | select(.age > 25)' data.jsonselect 只保留满足条件的元素。
jq -c '.items[]' data.json-c 压成单行 JSON,适合逐行传给下游命令。
jq 不是系统自带命令,需要装一下:
brew install jq # macOS
sudo apt install jq # Ubuntu / Debian
sudo dnf install jq # CentOS / Rocky Linux装完用 jq --version 确认,能看到类似 jq-1.8.1 的输出就说明可用了。
jq .拿到一段 JSON,别急着写路径。先 jq . 格式化一遍——它会缩进展开、语法上色,让你看清结构:哪些是对象、哪些是数组、字段叫什么。
curl -s https://api.example.com/user | jq .
jq . data.json看清结构,才知道接下来的路径该怎么写。jq . 顺便还能验证 JSON 合不合法——格式错了它会直接报错。管道用法里 . 可以省略,所以临时看接口响应经常直接写 curl -s ... | jq。
后面的例子都基于这份 data.json:
{
"name": "AHA",
"version": "1.0",
"enabled": true,
"users": [
{ "name": "Alice", "age": 25, "city": "Beijing" },
{ "name": "Bob", "age": 30, "city": "Shanghai" },
{ "name": "Charlie", "age": 28, "city": "Beijing" }
]
}. 取字段,[] 访问数组jq 的核心就是“沿着结构写路径”:
jq '.name' data.json # 取顶层字段 → "AHA"
jq '.users[0]' data.json # 数组第 0 个元素
jq '.users[-1]' data.json # 负数下标:最后一个元素
jq '.users[0:2]' data.json # 切片:前两个元素
jq '.users[]' data.json # 摊开数组的每个元素
jq '.users[].name' data.json # 摊开后取每个元素的 name. 是“进入一个字段”,可以一直往下走(.database.host);[] 是“展开数组”。组合起来就能定位到 JSON 里任意深处的值。
.users[].name 会逐行输出三个名字。如果希望结果仍然是一个 JSON 数组,在表达式外面套一层方括号:
jq '[.users[].name]' data.json["Alice", "Bob", "Charlie"]逐行输出适合接 Shell 管道,数组输出适合继续作为 JSON 传给其他程序——按下游需要选。
-r:把结果变成能用的裸字符串jq 默认输出带引号的 JSON 值——"abc" 而不是 abc。当你要把结果传给别的命令时,引号会碍事。-r(--raw-output)去掉引号:
jq '.name' data.json # 输出 "AHA"(带引号)
jq -r '.name' data.json # 输出 AHA(可直接用)最典型的用法是从登录接口提取 Token 再传给下一个请求:
token=$(curl -s https://example.com/login | jq -r '.token')
curl -H "Authorization: Bearer $token" https://example.com/api凡是 jq 的结果要赋给变量、或接着传给别的命令,基本都要加 -r。
jq 表达式里的 | 和 Shell 管道是一个思路:把左边的结果传给右边继续处理。
jq '.users | length' data.json # 先取数组,再算长度 → 3
jq '.users[] | select(.age >= 28) | .name' data.json # 摊开 → 筛选 → 取字段简单场景下 .users[].name 和 .users[] | .name 等价,但处理步骤一多,用管道分段写会清晰得多。长表达式还可以换行缩进,像写程序一样:
jq '
.users
| map(select(.age >= 28))
| sort_by(.age)
| map(.name)
' data.json读法就是数据的流动过程:取数组 → 筛选 → 排序 → 提取姓名。能看懂这一条,jq 的大部分用法都是这个模式的变体。
select() 只保留满足条件的元素,是 jq 里最重要的函数之一:
jq '.users[] | select(.age > 25)' data.json
jq '.users[] | select(.city == "Beijing")' data.json
jq '.users[] | select(.city != "Beijing")' data.json
# 多条件:and / or
jq '.users[] | select(.city == "Beijing" and .age >= 28)' data.json
# 字符串匹配:contains / 正则 test
jq '.users[] | select(.name | contains("li"))' data.json
jq '.users[] | select(.name | test("^A"))' data.jsonmap() 对数组的每个元素执行相同操作,并把结果重新组成数组:
jq '.users | map(.name)' data.json # 提取所有姓名
jq '.users | map(select(.age >= 28))' data.json # 筛选
jq '.users | map(select(.age >= 28) | .name)' data.json # 筛选 + 提取一步到位map(select(...)) 和 [.users[] | select(...)] 结果一样,前者在处理数组时通常写起来更自然。
jq 不只能读,还能按需生成新结构——把第三方接口的返回转成自己系统要的格式,靠的就是它:
jq '[.users[] | {username: .name, user_age: .age}]' data.json[
{ "username": "Alice", "user_age": 25 },
{ "username": "Bob", "user_age": 30 },
{ "username": "Charlie", "user_age": 28 }
]字符串可以用 + 拼接,也可以用 \(...) 插值:
jq -r '.users[] | "\(.name) lives in \(.city)"' data.jsonAlice lives in Beijing
Bob lives in Shanghai
Charlie lives in Beijing赋值操作可以改写 JSON:
jq '.version = "2.0"' data.json # 修改字段
jq '.environment = "production"' data.json # 新增字段
jq '.users[0].age = 26' data.json # 修改嵌套字段
jq '.users |= map(.age += 1)' data.json # 所有用户年龄加一
jq 'del(.enabled)' data.json # 删除字段
jq '.users |= map(del(.city))' data.json # 删除每个用户的 city|= 可以理解为“取出这个字段,处理一遍,再放回去”。del() 常用来清理接口返回里的敏感字段,比如密码和内部标识。
jq 默认只读,但管道写回要小心
jq 只把结果打到标准输出,不改原文件,可以放心练。要小心的是写回:不要写成 jq '.x=1' data.json > data.json——> 会在 jq 读文件之前就清空 data.json,结果是原文件直接变空(见 管道与重定向)。要原地改 JSON,先写临时文件再 mv 覆盖:
jq '.version = "2.0"' data.json > data.tmp && mv data.tmp data.jsonjq '.users | length' data.json # 数量 → 3
jq '[.users[].age] | add' data.json # 年龄总和
jq '[.users[].age] | add / length' data.json # 平均年龄
jq '[.users[].age] | max' data.json # 最大值(min 同理)
jq '.users | sort_by(.age)' data.json # 按年龄排序
jq '.users | sort_by(.age) | reverse' data.json # 倒序
jq '.users | max_by(.age)' data.json # 年龄最大的用户
jq '.users | unique_by(.city)' data.json # 按城市去重(结果会被排序)分组统计用 group_by,比如按城市统计人数:
jq '.users | group_by(.city) | map({city: .[0].city, count: length})' data.json[
{ "city": "Beijing", "count": 2 },
{ "city": "Shanghai", "count": 1 }
]真实接口的字段不一定总是存在。直接读缺失字段会得到 null,用 // 可以兜底:
jq '.description' data.json # null
jq -r '.description // "暂无描述"' data.json # 暂无描述如果结构本身可能不存在(比如某些对象没有这个数组),在表达式后加 ? 可以吞掉报错:
jq '.users[]?.name?' data.json处理字段结构不稳定的接口响应时,这两个写法能省掉很多防御性判断。
不要把 Shell 变量直接拼进 jq 表达式字符串里——引号嵌套很容易出错,值里带特殊字符还会注入表达式。正确做法是 --arg:
username="Alice"
jq --arg name "$username" '.users[] | select(.name == $name)' data.json--arg 传进去的一律是字符串;数字、布尔、数组要用 --argjson:
age=28
jq --argjson min_age "$age" '.users[] | select(.age >= $min_age)' data.json很多应用输出一行一条的 JSON 日志。jq 天然适合这种格式——它会把每行当作一个独立的 JSON 依次处理:
tail -f app.log | jq # 实时格式化
tail -f app.log | jq 'select(.level == "ERROR")' # 只看错误配合字符串插值,可以把 JSON 日志变回人类友好的格式:
tail -f app.log | jq -r 'select(.level == "ERROR") | "\(.time) \(.message)"'2026-07-29T10:00:05 数据库连接失败如果日志里混有普通文本行(启动 banner、堆栈……),直接 jq 会解析报错。用 -R 把每行当纯文本读入,再 fromjson? 尝试解析、失败就跳过:
tail -f app.log | jq -Rr 'fromjson? | select(.level == "ERROR") | .message'顺带一提:日志里常见的 \u670d\u52a1 这类 Unicode 转义(其实就是“服务”两个字),jq 解析后会自动还原成中文,不用额外处理。
| 参数 | 作用 |
|---|---|
-r | 字符串按纯文本输出,不带引号 |
-c | 紧凑单行输出,适合 JSON Lines 和管道传递 |
-R | 把输入当普通文本逐行读取 |
-s | 把全部输入读成一个数组再处理 |
-e | 结果为 false/null 时返回非零退出码 |
-n | 不读输入,直接执行表达式(常用于构造 JSON) |
-e 让 jq 可以直接当条件判断用,在脚本和 CI 里很常见:
if jq -e '.enabled == true' data.json > /dev/null; then
echo "功能已启用"
fi-s 则可以把多个文件或多行 JSON 合并成数组处理:
jq -s '.' file1.json file2.json # 合并成数组
jq -s 'add' file1.json file2.json # 两个数组文件拼成一个curl 链式调用——请求接口、提取字段、继续请求:
user_id=$(curl -s https://example.com/api/users | jq -r '.data.users[0].id')
curl -s "https://example.com/api/users/$user_id" | jq批量下载——提取 URL 列表逐个处理。URL 可能含特殊字符,逐行读比 xargs 空格切分更稳:
jq -r '.files[].url' files.json |
while IFS= read -r url; do
curl -O "$url"
donekubectl / docker——云原生工具都支持 -o json,配 jq 就是现成的查询语言:
kubectl get pods -o json | jq -r '.items[].metadata.name'
docker inspect $(docker ps -q) | jq -r '.[] | "\(.Name) \(.Config.Image)"'对象转环境变量格式——to_entries 把对象拆成键值对数组:
jq -r 'to_entries[] | "\(.key)=\(.value)"' config.jsonHOST=127.0.0.1
PORT=8080| 需求 | grep 的问题 | jq 的做法 |
|---|---|---|
| 取某个字段的值 | JSON 可能压成一行,或字段跨行 | jq -r '.field' 按路径取,不管格式 |
| 取数组里第 N 个 | grep 没有“第几个”的概念 | jq '.arr[N]' |
| 按条件筛选元素 | 无法理解结构和类型 | jq 'select(...)' 按值和类型判断 |
| 统计、排序、分组 | 需要拼一串 sort/uniq/wc | length、sort_by、group_by 一步到位 |
结论很简单:结构化数据用结构化工具。grep 适合纯文本日志,JSON 交给 jq。
| 误区 | 更稳妥的做法 |
|---|---|
用 grep 从 JSON 里抠字段 | JSON 用 jq 按路径取,不受换行和缩进影响 |
不先 jq . 就直接猜路径 | 先格式化看清结构,再写 .a.b[].c 路径 |
| jq 结果传给别的命令时带着引号 | 要裸值加 -r,否则引号会干扰下游命令 |
jq '...' f.json > f.json 想原地改 | > 会先清空文件,先写临时文件再 mv 覆盖 |
| 表达式用双引号包裹 | 用单引号,避免 $ 和反斜杠被 Shell 提前解析 |
| 把 Shell 变量拼进表达式字符串 | 用 --arg / --argjson 传入,安全且不用纠结引号 |
| 拿 jq 解析 YAML 或 JS 对象字面量 | jq 只认合法 JSON——字段名和字符串必须用双引号 |
版权归属:Shuo Liu