Bash скрипты: Парсинг JSON в Bash

В современной веб-разработке и системном администрировании JSON (JavaScript Object Notation) стал стандартом де-факто для обмена данными между сервисами. REST API, конфигурационные файлы современных приложений (Docker, Kubernetes, Terraform), логи серверов — все это активно использует JSON. Однако сценарии обслуживания сервера, автоматизации резервного копирования или мониторинга часто пишутся на классическом bash.

Перед администратором хостинга и DevOps-инженером встает классическая задача: как извлечь конкретное значение из JSON-ответа curl, не устанавливая тяжелые интерпретаторы Python или Node.js? Как обработать массив IP-адресов в bash-скрипте, выполняющемся в окружении BusyBox? Как валидировать конфигурацию клиента прямо во время выполнения хука развертывания?

В этой статье мы разберем все легальные и надежные способы парсинга JSON в Bash — от встроенных инструментов (awk/sed) до легковесных утилит (jqgojqjc), а также рассмотрим, как правильно писать скрипты для 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"

Как это работает:

  1. grep -o вырезает фрагмент "key": значение.

  2. Первый sed удаляет все до двоеточия.

  3. Второй sed снимает кавычки.

Ограничения:

  • Не работает со значениями, содержащими запятые или фигурные скобки.

  • Сломается на строке {"msg":"Hello, world!"} (из-за запятой).

  • Не различает nulltruefalse.

Использование 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 — это не «костыль», а стандартная задача современного администрирования. Правильный подход:

  1. Для production-скриптов на VPS/выделенном сервере: используйте jq. Это быстро, надежно и поддерживается всеми дистрибутивами.

  2. Для окружений с ограниченными ресурсами (роутеры, busybox): применяйте компактные альтернативы (gojq) или однострочники на Python.

  3. Для одноразовых задач на клиентской машине без прав root: подойдет python -c или самодельная Bash-функция, но с пониманием рисков.

  • 0 Пользователи нашли это полезным

Помог ли вам данный ответ?

Ищете что-то другое?

xvps.ru