jq и yq: JSON и YAML в shell
В data engineering 80% interchange format — JSON. API возвращает JSON, Airflow конфигурируется JSON-ом, Kafka events часто JSON, CloudWatch logs тоже JSON. Без удобного инструмента для shell это превращается в боль: парсить grep’ом не работает (line-oriented vs structured), python-one-liner громоздкий, sed/awk не понимают вложенность.
jq решает это. Это sed/awk для JSON — компактный язык запросов, который встраивается в pipelines, обрабатывает gigabyte-файлы стрим-режимом, делает фильтрацию/трансформацию/агрегацию за одну команду.
yq — то же самое для YAML. Конфиги Kubernetes, GitHub Actions workflows, Ansible playbooks — всё YAML. yq позволяет редактировать их из CI/скриптов программно.
В этом уроке: jq основы (фильтры, селекторы, типы), частые DE-паттерны, jsonl streaming, и yq для YAML.
Установка
# Debian/Ubuntu:
sudo apt install jq
# macOS:
brew install jq
# yq (Mike Farah, Go-based, де-факто стандарт):
brew install yq
# или: snap install yq
# или: docker run --rm -v "$PWD:/workdir" mikefarah/yq ...
Существуют два разных yq: 1) Python-based python-yq (https://github.com/kislyuk/yq) — wrapper на jq для YAML, с jq-синтаксисом. 2) Go-based mikefarah/yq (https://github.com/mikefarah/yq) — отдельный язык, не совместим с jq. В большинстве туториалов и production используется mikefarah/yq — он быстрее, без зависимостей Python. Проверь установленную версию: yq --version (если упоминает Mike Farah — Go-based).
jq основы: pretty-print, identity
# Pretty-print произвольного JSON:
curl -s https://api.github.com/repos/torvalds/linux | jq '.'
. — identity filter, “вернуть как есть”. Если вход — minified JSON, выход — отформатированный с отступами и подсветкой (если stdout — TTY).
# Минифицировать обратно:
echo '{"a": 1, "b": 2}' | jq -c '.'
# {"a":1,"b":2}
-c — compact, одна строка. Полезно для логов и pipeline-stream.
Field access
echo '{"name": "alice", "age": 30}' | jq '.name'
# "alice"
Вложенные поля:
echo '{"user": {"name": "alice", "email": "[email protected]"}}' | jq '.user.email'
# "[email protected]"
Опциональный доступ (если поле может отсутствовать):
echo '{}' | jq '.missing'
# null
echo '{}' | jq '.missing?'
# null (no error)
echo '{}' | jq '.deep.missing'
# null
echo '{}' | jq '.deep.missing // "default"'
# "default" (// — coalesce operator, как ?? в JS/Python)
Raw output (-r)
Без -r строковые значения jq возвращает с кавычками:
echo '{"name": "alice"}' | jq '.name'
# "alice" <- с кавычками
echo '{"name": "alice"}' | jq -r '.name'
# alice <- без
-r (raw) обязателен, когда выход jq идёт в bash-переменную:
USERNAME=$(curl -s ... | jq -r '.user.name')
# Без -r получишь "alice" с кавычками внутри переменной, и аргументы поломаются
Правило: в pipeline в bash почти всегда нужен -r. Кавычки нужны только когда передаёшь output в другую jq команду или другой JSON parser, который сам ожидает quoted strings.
Arrays: iteration и индексы
data='[{"name":"alice"},{"name":"bob"},{"name":"carol"}]'
# Доступ по индексу:
echo "$data" | jq '.[0]' # {"name":"alice"}
echo "$data" | jq '.[1].name' # "bob"
echo "$data" | jq '.[-1]' # {"name":"carol"} (last)
# Slice:
echo "$data" | jq '.[0:2]' # первые два
# Развернуть массив в поток отдельных объектов:
echo "$data" | jq '.[]'
# {"name":"alice"}
# {"name":"bob"}
# {"name":"carol"}
# Длина:
echo "$data" | jq 'length' # 3
.[] — iterator, “развернуть в стрим”. Это ключевой оператор: вместо одного output (массив) jq выдаёт N outputs (по одному на элемент). Каждый можно обработать дальше.
Filter и select
users='[{"name":"alice","age":30},{"name":"bob","age":17},{"name":"carol","age":25}]'
# Взрослые (age >= 18):
echo "$users" | jq '.[] | select(.age >= 18)'
# {"name":"alice","age":30}
# {"name":"carol","age":25}
# Только имена взрослых:
echo "$users" | jq '.[] | select(.age >= 18) | .name'
# "alice"
# "carol"
# С -r — без кавычек:
echo "$users" | jq -r '.[] | select(.age >= 18) | .name'
# alice
# carol
| в jq — pipe внутри выражения, как pipe в shell. Каждый этап принимает input от предыдущего.
Сложные предикаты
# AND:
jq '.[] | select(.age >= 18 and .country == "RU")'
# OR:
jq '.[] | select(.status == "active" or .status == "pending")'
# IN-список (через any):
jq '.[] | select(.tag | IN("a","b","c"))'
# Regex match:
jq '.[] | select(.email | test("@example\\.com$"))'
# Negation:
jq '.[] | select(.age >= 18) | select(.country != "blocked_country")'
Трансформация: map, объекты, конструкторы
# Удвоить все числа:
echo '[1,2,3]' | jq '[.[] * 2]'
# [2, 4, 6]
# map() — синтаксический сахар:
echo '[1,2,3]' | jq 'map(. * 2)'
# [2, 4, 6]
# Реструктурировать объекты:
users='[{"first":"alice","last":"smith","age":30}]'
echo "$users" | jq '.[] | {name: (.first + " " + .last), age}'
# {"name": "alice smith", "age": 30}
Заметь: {name: ..., age} — короткий синтаксис. age без двоеточия — age: .age.
# Переименовать поле:
jq '.[] | {user_name: .name, user_age: .age}'
# Добавить поле:
jq '.[] | . + {tag: "production"}'
# Удалить поле:
jq '.[] | del(.password)'
DE-паттерн 1: API -> JSONL для downstream
API возвращает массив, downstream ожидает JSONL (one JSON object per line). Стандартный pattern data ingestion.
curl -s https://api.example.com/users \
| jq -c '.users[]' \
> users.jsonl
-c — compact (one line per output). .users[] — развернуть массив users в поток. Каждый объект уходит отдельной строкой. Результат:
{"id":1,"name":"alice"}
{"id":2,"name":"bob"}
{"id":3,"name":"carol"}
JSONL — золотой стандарт для:
- Snowflake COPY — встроенный JSONL ingestion.
- BigQuery load jobs —
--source_format=NEWLINE_DELIMITED_JSON. - Spark/DuckDB —
read_jsonпонимает JSONL быстрее, чем nested arrays. - Streaming: можно обрабатывать построчно, не грузя в память весь массив.
DE-паттерн 2: агрегация (group-by)
events='[
{"type":"click","user":"alice"},
{"type":"view","user":"bob"},
{"type":"click","user":"alice"},
{"type":"click","user":"carol"}
]'
# Count by type:
echo "$events" | jq 'group_by(.type) | map({type: .[0].type, count: length})'
# [
# {"type":"click","count":3},
# {"type":"view","count":1}
# ]
jq может многое, что SQL делает запросами.
DE-паттерн 3: csv export
# JSON -> CSV для Excel/manual review:
echo '[{"name":"alice","age":30},{"name":"bob","age":25}]' \
| jq -r '.[] | [.name, .age] | @csv'
# "alice",30
# "bob",25
# С заголовком:
echo '[{"name":"alice","age":30},{"name":"bob","age":25}]' \
| jq -r '["name","age"], (.[] | [.name, .age]) | @csv'
# "name","age"
# "alice",30
# "bob",25
@csv — встроенный formatter. Экранирует кавычки, обрамляет strings. Аналогично есть @tsv, @uri, @html, @sh.
@sh особенно полезен для безопасного передачи jq-значений в bash:
# Безопасный путь — jq экранирует strings для shell:
eval "args=( $(jq -r '.[] | @sh' < items.json) )"
# Теперь args — массив bash, корректно с любыми символами в значениях
DE-паттерн 4: large-file stream processing
Для больших файлов (1 GB JSON-логов) не загружай в память — стримь:
# Streaming mode — последовательная обработка без памяти на весь файл:
jq --stream -c 'select(length==2)' huge_logs.json
--stream режим выдаёт path-value пары, что позволяет процессить multi-gigabyte файлы. Сложнее в синтаксисе, но необходим для больших данных.
Простая альтернатива — если файл уже JSONL (каждая запись на отдельной строке):
# Обработать построчно:
cat huge.jsonl | jq -c 'select(.status == "error")' > errors.jsonl
jq читает построчно, обрабатывает каждый JSON независимо. Память — на одну строку.
Если работаешь с большими файлами и jq оказывается медленным, посмотри на jaq (Rust-based) или gojq (Go) — drop-in replacement для jq, в 2-5 раз быстрее на больших файлах. Синтаксис тот же.
Реальный DE-script с jq
#!/bin/bash
set -euo pipefail
# Fetch users, filter active, export to JSONL with normalized schema
curl --fail --max-time 30 -H "Authorization: Bearer $API_TOKEN" \
https://api.example.com/users \
| jq -c '.users[]
| select(.status == "active")
| {
id,
email: (.email | ascii_downcase),
name: (.first_name + " " + .last_name),
created_at,
plan: (.plan // "free")
}' \
> users_$(date +%Y%m%d).jsonl
wc -l users_*.jsonl # сколько записей
Что тут происходит:
curl --fail— упасть на HTTP 4xx/5xx..users[]— развернуть массив.select(.status == "active")— фильтр.{...}— нормализованный output: id, email (lowercase), name (concat), created_at (as is), plan (с default “free”).-c— compact (один объект на строку, JSONL).
Один pipeline — fetch + parse + filter + transform + save. На Python было бы 30+ строк.
yq: то же самое для YAML
yq от Mike Farah — Go-based, отдельный язык (НЕ jq-compatible несмотря на похожий синтаксис).
# Pretty-print:
yq '.' config.yaml
# Field access:
yq '.database.host' config.yaml
# Изменить значение (in-place):
yq -i '.database.host = "newhost.example.com"' config.yaml
# Добавить элемент в массив:
yq -i '.users += [{"name": "alice", "role": "admin"}]' config.yaml
# Конвертация YAML <-> JSON:
yq -o json config.yaml > config.json
yq -p json -o yaml config.json > config.yaml
Real DE use case: edit Kubernetes manifest in CI
# Обновить image tag в deployment.yaml перед apply:
yq -i ".spec.template.spec.containers[0].image = \"myimage:${BUILD_ID}\"" deployment.yaml
# Применить:
kubectl apply -f deployment.yaml
Без yq это было бы sed-magic с регулярками, что хрупко и страшно.
Tips & gotchas
1. Кавычки внутри jq filter в bash
Если jq-filter содержит double quotes, используй single quotes снаружи:
# OK:
jq '.users[] | select(.role == "admin")'
# Сломается:
jq ".users[] | select(.role == \"admin\")" # экранирование нужно, hell
2. null vs missing vs empty
echo '{}' | jq '.missing' # null
echo '{"x":null}' | jq '.x' # null
# Различение через has():
echo '{}' | jq 'has("x")' # false
echo '{"x":null}' | jq 'has("x")' # true
# Очистка nulls:
jq 'del(..|nulls)'
3. -e flag — strict exit codes
# Без -e: jq всегда возвращает 0 если выражение валидно
jq '.missing' < data.json # вывод null, exit 0
# С -e: exit 1 если результат null/false
jq -e '.users | length > 0' < data.json
if [ $? -ne 0 ]; then echo "No users!"; fi
jq -e критичен для validation в скриптах: позволяет fail loudly если данные не такие, как ожидалось.
4. Сравнение типов
# Числа vs строки — разные типы:
echo '{"x":"5"}' | jq '.x > 3' # error! string vs number
# Конверсия:
echo '{"x":"5"}' | jq '(.x | tonumber) > 3' # true
Cross-links
- Модуль 11 (networking) — curl + jq — основа любого API ingestion.
- Урок 02 (fzf) — fuzzy search для JSON-логов часто комбинируется с jq.
- Модуль 17 (advanced scripting) — production-paterns для скриптов с jq.
Попробуй сам
- Скачай GitHub users API ответ и поэкспериментируй:
curl -s https://api.github.com/users/torvalds | jq '.'
curl -s https://api.github.com/users/torvalds | jq '.public_repos'
curl -s https://api.github.com/users/torvalds | jq -r '.login, .name, .public_repos'
- Получи список репозиториев пользователя и выведи только имена:
curl -s https://api.github.com/users/torvalds/repos | jq -r '.[].name' | head -20
- Сделай JSONL из массива:
curl -s https://api.github.com/users/torvalds/repos | jq -c '.[]' > repos.jsonl
wc -l repos.jsonl
- Фильтр + projection:
curl -s https://api.github.com/users/torvalds/repos \
| jq -r '.[] | select(.stargazers_count > 100) | "\(.name): \(.stargazers_count)"' \
| sort -t: -k2 -n -r
- YAML edit:
cat > test.yaml <<EOF
version: 1.0
services:
api:
image: myapi:old
replicas: 3
EOF
yq '.services.api.image' test.yaml
yq -i '.services.api.image = "myapi:new"' test.yaml
cat test.yaml