В современной веб-разработке и системном администрировании JSON (JavaScript Object Notation) стал стандартом де-факто для обмена данными между сервисами. REST API, конфигурационные файлы современных приложений (Docker, Kubernetes, Terraform), логи серверов — все это активно использует JSON. Однако сценарии обслуживания сервера, автоматизации резервного копирования или мониторинга часто пишутся на классическом bash.
Перед администратором хостинга и DevOps-инженером встает классическая задача: как извлечь конкретное значение из JSON-ответа curl, не устанавливая тяжелые интерпретаторы Python или Node.js? Как обработать массив IP-адресов в bash-скрипте, выполняющемся в окружении BusyBox? Как валидировать конфигурацию клиента прямо во время выполнения хука развертывания?
В этой статье мы разберем все легальные и надежные способы парсинга JSON в Bash — от встроенных инструментов (awk/sed) до легковесных утилит (jq, gojq, jc), а также рассмотрим, как правильно писать скрипты для production-среды на хостинге.
Данная информация предназначена для услуг: VPS хостинг или Облачный хостинг
Почему нельзя парсить JSON стандартными textutils (grep/sed/awk)?
Первое правило администрирования Unix: не парсите структурированные форматы регулярными выражениями. JSON — контекстно-свободный язык, в то время как регулярные выражения описывают лишь регулярные языки.
Рассмотрим простой пример. Допустим, мы получили JSON:
{"name":"John \"The Great\" Doe","age":30,"city":"New York"}
Попытка извлечь имя через grep -oP '"name":"\K[^"]*' вернет John \, что сломает скрипт из-за экранированной кавычки. Проблемы усугубляются:
-
Вложенность объектов и массивов.
-
Многострочные строки.
-
Юникод-символы.
-
Разный порядок ключей.
-
Числа с плавающей точкой без кавычек.
Единственный безопасный случай использования sed/grep: вы полностью контролируете генерацию JSON (например, пишете echo '{"status":"ok"}' сами). Для внешних API — категорически нет.
1.1. Когда awk еще может спасти (и не навредить)
awk имеет конечный автомат, но с его помощью можно извлечь значения из простейших линейных JSON, где гарантированно нет вложенности:
echo '{"ip":"192.168.1.1","port":8080}' | awk -F'"' '/"ip"/ {print $4}'
Этот код выведет 192.168.1.1. Но стоит появиться массиву {"ips":["192.168.1.1","10.0.0.1"]} — метод сломается.
Утилита jq — стандарт де-факто для JSON в консоли
jq — это легковесный и очень быстрый процессор JSON, написанный на C. Он устанавливается одной командой и не имеет зависимостей, кроме libc.
Установка на популярные ОС хостинга
Debian/Ubuntu (apt):
sudo apt update && sudo apt install jq -y
CentOS/RHEL/AlmaLinux (yum/dnf):
sudo dnf install jq -y
# или для старых версий
sudo yum install epel-release -y && sudo yum install jq -y
Alpine Linux (часто используется в контейнерах):
apk add jq
Без прав root (статически собранный бинарник):
wget https://github.com/jqlang/jq/releases/download/jq-1.7/jq-linux-amd64 -O ~/jq
chmod +x ~/jq
export PATH="$HOME:$PATH"
Синтаксис и базовые фильтры
Основная концепция: jq получает JSON на stdin и применяет фильтр. Простейший фильтр — . (вывести всё).
echo '{"server": "web1", "port": 443}' | jq '.'
Результат будет отформатирован (pretty-print) с отступами.
Доступ к свойству:
echo '{"server": "web1"}' | jq '.server' # "web1"
echo '{"server": "web1"}' | jq -r '.server' # web1 (raw output, без кавычек)
Обработка массивов:
curl -s https://api.github.com/repos/jqlang/jq/commits | jq '.[0].commit.message'
Получим сообщение первого коммита.
Продвинутые фильтры: map, select, join
Для базы знаний хостинга критически полезны примеры из практики.
Пример 1. Получение списка IP-адресов серверов из инвентаря Ansible:
# inventory.json содержит список хостов
cat inventory.json | jq -r '.servers[] | select(.role == "database") | .ip'
Пример 2. Преобразование JSON в CSV для импорта в Excel:
curl -s 'https://api.example.com/users' | jq -r '.[] | [.id, .name, .email] | @csv'
Пример 3. Фильтрация по числовым значениям (логи мониторинга):
# Выбрать события с кодом ответа > 500
cat access_logs.json | jq 'select(.status >= 500) | {time: .timestamp, uri: .request_uri}'
Работа с большими файлами (streaming)
Если JSON-файл лога на диске весит несколько гигабайт, jq по умолчанию загрузит его в память. Для потоковой обработки используйте --stream:
jq --stream -n 'fromstream(2 | truncate_stream(inputs))' huge.json
Это сложный пример, но для 99% задач (логи до 1 ГБ) стандартный режим подходит.
Парсинг JSON без установки jq (чистый Bash + grep/sed для ограниченных сред)
Ситуация: вы зашли на сервер клиента по SSH, jq не установлен, прав на установку нет, а извлечь одно значение нужно срочно. На помощь приходит Bash-функция, использующая встроенный механизм работы со строками.
Важное предупреждение: Этот метод работает ТОЛЬКО для плоских JSON-объектов (без вложенных массивов и объектов). Если есть вложенность — используйте python -m json.tool.
Функция parse_json на чистом Bash
#!/bin/bash
parse_json() {
local json key value
json="$1"
key="$2"
# Ищем ключ, захватываем значение до запятой или закрывающей скобки
value=$(echo "$json" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*[^,}]*" | sed -E 's/.*:[[:space:]]*//' | sed -E 's/^"//;s/"$//')
echo "$value"
}
# Пример использования:
response='{"status":"active","ip":"10.0.0.2","port":3306}'
ip=$(parse_json "$response" "ip")
echo "MySQL IP: $ip"
Как это работает:
-
grep -oвырезает фрагмент"key": значение. -
Первый
sedудаляет все до двоеточия. -
Второй
sedснимает кавычки.
Ограничения:
-
Не работает со значениями, содержащими запятые или фигурные скобки.
-
Сломается на строке
{"msg":"Hello, world!"}(из-за запятой). -
Не различает
null,true,false.
Использование Python (есть на 99% хостингов)
Python — это «джокер» в мире хостинга. Он предустановлен почти на всех VPS и shared-хостингах.
Однострочник для извлечения значения:
echo '{"ip":"192.168.1.100"}' | python3 -c "import sys, json; print(json.load(sys.stdin)['ip'])"
Безопасное извлечение с обработкой ошибок (скрипт):
#!/bin/bash
json_string='{"server":{"name":"web","port":80}}'
port=$(echo "$json_string" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
print(data.get('server', {}).get('port', ''))
except:
print('')
")
echo "Port: $port"
Когда использовать Python: для сложной логики (обход массивов, условные преобразования), если jq отсутствует и нет времени на его установку.
Реальные кейсы из практики хостинга
Скрипт мониторинга: проверка статуса через API и отправка в Zabbix
Администратору нужно каждые 5 минут проверять статус платежного шлюза.
#!/bin/bash
# monitor_gateway.sh
API_URL="https://api.payment.com/v1/status"
API_KEY="secret_key_123"
response=$(curl -s -H "Authorization: Bearer $API_KEY" "$API_URL")
status=$(echo "$response" | jq -r '.status')
latency=$(echo "$response" | jq -r '.metrics.latency_ms // 0')
if [ "$status" != "operational" ]; then
logger -t mon_gateway "CRITICAL: Gateway status is $status"
# Отправка в Zabbix через zabbix_sender
echo "- gateway.status $status" | zabbix_sender -c /etc/zabbix/zabbix_agentd.conf -i - 2>&1
fi
echo "$latency" > /var/run/gateway_latency_ms
Автоматическое обновление DNS-записей (Cloudflare API)
Клиент хостинга использует динамический DNS. Скрипт на bash получает текущий IP и обновляет A-запись.
#!/bin/bash
ZONE_ID="your_zone_id"
RECORD_NAME="home.example.com"
API_TOKEN="your_api_token"
# Получаем реальный IP (проверяем несколько сервисов)
CURRENT_IP=$(curl -s https://api.ipify.org || curl -s https://icanhazip.com)
# Получаем ID записи и текущий IP из Cloudflare
RECORD_INFO=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$RECORD_NAME" \
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json")
RECORD_ID=$(echo "$RECORD_INFO" | jq -r '.result[0].id')
EXISTING_IP=$(echo "$RECORD_INFO" | jq -r '.result[0].content')
if [ "$CURRENT_IP" != "$EXISTING_IP" ]; then
# Обновляем запись
UPDATE_RESULT=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"$RECORD_NAME\",\"content\":\"$CURRENT_IP\",\"ttl\":120,\"proxied\":false}")
SUCCESS=$(echo "$UPDATE_RESULT" | jq -r '.success')
if [ "$SUCCESS" = "true" ]; then
echo "DNS обновлен: $EXISTING_IP -> $CURRENT_IP"
else
echo "Ошибка обновления DNS" >&2
fi
fi
Парсинг логов Nginx в формате JSON
Современные настройки логов Nginx могут выглядеть так:
log_format main escape=json '{ "@timestamp": "$time_iso8601", "remote_addr": "$remote_addr", "request": "$request", "status": $status, "body_bytes_sent": $body_bytes_sent, "http_referer": "$http_referer", "http_user_agent": "$http_user_agent" }';
Скрипт анализа для поиска подозрительных запросов:
#!/bin/bash
# find_attacks.sh
LOGFILE="/var/log/nginx/access.json"
# Извлечь все попытки SQL-инъекций за последние 10 минут
ten_min_ago=$(date -d '10 minutes ago' --iso-8601=seconds | cut -d'+' -f1)
jq --arg time "$ten_min_ago" 'select(.["@timestamp"] > $time) | select(.request | test("union.*select|drop.*table|sleep\\(\\d+\\)"; "i"))' "$LOGFILE"
Валидация конфигурации клиента перед деплоем
В панели управления хостингом клиент загружает JSON-конфиг для своего PHP-приложения. Хук предварительной проверки:
#!/bin/bash
validate_config() {
local config_file="$1"
# Проверяем валидность JSON
if ! jq empty "$config_file" 2>/dev/null; then
echo "Ошибка: Файл $config_file не является валидным JSON"
return 1
fi
# Схема: обязательные поля
required_keys=("database_host" "database_name" "app_key")
for key in "${required_keys[@]}"; do
value=$(jq -r ".$key // empty" "$config_file")
if [ -z "$value" ]; then
echo "Ошибка: Отсутствует обязательное поле '$key'"
return 1
fi
done
# Доп. проверка: тип данных
port=$(jq -r '.port // 0' "$config_file")
if ! [[ "$port" =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "Предупреждение: port должен быть числом от 1 до 65535, установлен '$port'"
fi
echo "Конфигурация валидна."
return 0
}
validate_config "/home/client/config.json"
Оптимизация производительности при массовом парсинге
В базу знаний хостинга стоит добавить раздел о том, как не убить сервер клиента.
Пакетная обработка (избегаем вызов jq в цикле)
Плохо (медленно, 1000 вызовов jq):
for id in $(cat ids.txt); do
curl -s "https://api.com/user/$id" | jq '.name'
done
Хорошо (один вызов jq):
# Собираем все ответы в один JSON-массив
> all_users.json
for id in $(cat ids.txt); do
curl -s "https://api.com/user/$id" >> all_users.json
echo "" >> all_users.json # разделитель
done
# Используем jq с флагом --slurp
jq -s 'map(.name)' all_users.json
Работа с потоками без создания временных файлов
# Отдаем ответ curl напрямую в jq
curl -s 'https://api.example.com/stream' | jq -c 'select(.type == "metric") | {time, value}'
Альтернативы jq для embedded-систем
На роутерах или в контейнерах с минимальным дисковым пространством:
-
jq (динамический бинарник ~ 2 МБ)
-
gojq (на Go, 5 МБ, но не требует libc)
-
jc (JSON converter, парсит вывод утилит)
Установка gojq:
wget https://github.com/itchyny/gojq/releases/download/v0.12.13/gojq_v0.12.13_linux_amd64.tar.gz
tar xzf gojq_v0.12.13_linux_amd64.tar.gz
sudo mv gojq /usr/local/bin/
Обработка ошибок и отладка скриптов
Скрипты парсинга JSON должны быть устойчивы к сетевым сбоям и изменениям API.
Безопасное извлечение с fallback-значениями
#!/bin/bash
set -euo pipefail # строгий режим
get_json_value() {
local json="$1"
local key="$2"
local default="$3"
local value
value=$(echo "$json" | jq -r ".$key // empty" 2>/dev/null)
if [ -z "$value" ]; then
echo "$default"
else
echo "$value"
fi
}
response=$(curl -s -f "https://api.example.com/status" || echo '{}')
rate_limit=$(get_json_value "$response" "rate_limit.remaining" "0")
echo "Осталось запросов: $rate_limit"
Логирование ошибок jq
result=$(curl -s "$URL" | jq '.data' 2> /tmp/jq_error.log)
if [ $? -ne 0 ]; then
logger -t "json_parser" "Ошибка jq: $(cat /tmp/jq_error.log)"
exit 1
fi
Проверка наличия инструментов в начале скрипта
#!/bin/bash
check_deps() {
local deps=("curl" "jq")
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
echo "Ошибка: '$dep' не установлен. Установите: apt install $dep или yum install $dep" >&2
exit 1
fi
done
}
check_deps
Интеграция с панелью управления хостингом (ISPmanager, cPanel, Plesk)
В базу знаний хостинга часто включают примеры для автоматизации через API самой панели.
Создание FTP-пользователя через API в Plesk (JSON RPC)
#!/bin/bash
PLESK_HOST="https://your-plesk:8443"
API_KEY="secret"
# Тело JSON-запроса
read -r -d '' JSON_DATA << EOM
{
"jsonrpc": "2.0",
"method": "ftp_user_create",
"params": {
"domain": "example.com",
"login": "backup_user",
"password": "StrongP@ss123",
"home": "/var/www/vhosts/example.com/backups"
},
"id": 1
}
EOM
response=$(curl -s -k -X POST "$PLESK_HOST/enterprise/control/agent.php" \
-H "HTTP_AUTH_LOGIN: $API_KEY" \
-H "Content-Type: application/json" \
-d "$JSON_DATA")
result=$(echo "$response" | jq -r '.result.status')
if [ "$result" = "ok" ]; then
echo "FTP-пользователь создан."
else
echo "Ошибка: $(echo "$response" | jq -r '.result.error')"
fi
Получение статистики использования диска через API cPanel
#!/bin/bash
CPANEL_USER="client1"
API_TOKEN="token"
DOMAIN="client1.example.com"
curl -s "https://cpanel.server.com:2087/json-api/cpanel?cpanel_jsonapi_user=$CPANEL_USER&cpanel_jsonapi_apiversion=2&cpanel_jsonapi_module=AccountSummary&cpanel_jsonapi_func=getaccountinfo" \
-H "Authorization: whm $API_TOKEN:$CPANEL_USER" | jq '.cpanelresult.data[0].diskused'
Часто задаваемые вопросы (FAQ) по парсингу JSON в Bash
Вопрос 1: Почему jq выводит null вместо значения?
Ответ: Ключ не найден. Используйте // empty или проверяйте if .key != null then .key else "default" end.
Вопрос 2: Как спарсить JSON из переменной, содержащей спецсимволы?
Ответ: Всегда передавайте JSON через <<< (here-string) и экранируйте двойные кавычки:
var='{"path":"C:\\Program Files"}'
jq '.path' <<< "$var"
Вопрос 3: Мой JSON содержит BOM (Byte Order Mark). jq ругается.
Ответ: Удалите BOM через sed или dos2unix:
sed '1s/^\xEF\xBB\xBF//' file_with_bom.json | jq .
Вопрос 4: Как вывести несколько значений в одной строке без кавычек в jq?
Ответ: Используйте \(.field) внутри строки:
echo '{"a":1,"b":2}' | jq -r '"\(.a),\(.b)"'
# Вывод: 1,2
Вопрос 5: Есть ли GUI-инструмент для тестирования фильтров jq?
Ответ: Онлайн-песочница: jqplay.org. Для терминала используйте jq -n 'фильтр'.
Заключение
Парсинг JSON в Bash — это не «костыль», а стандартная задача современного администрирования. Правильный подход:
-
Для production-скриптов на VPS/выделенном сервере: используйте
jq. Это быстро, надежно и поддерживается всеми дистрибутивами. -
Для окружений с ограниченными ресурсами (роутеры, busybox): применяйте компактные альтернативы (
gojq) или однострочники на Python. -
Для одноразовых задач на клиентской машине без прав root: подойдет
python -cили самодельная Bash-функция, но с пониманием рисков.