Памятка по работе с JSON в консоли Linux на примере api

Всем привет! Язык разметки JSON (что такое JSON отлично подано в другой статье на Хабр)используется в огромном количестве приложений и систем благодаря своей простоте (например, Docker использует его для описания конфигурации контейнеров), а также является стандартным форматом обмена данными между клиентом и сервером в RESTful API. В зависимости от платформы сервера, исходные данные могут иметь любой формат, а перед отправкой конвертироваться в JSON, или другой, который будет запрошен клиентом, и если это поддерживается на стороне сервера.

Ранее уже писал статью, как на базе PowerShell можно создать Web/API сервер, где наглядно можно посмотреть, как реализуется механизм конвертации данных на основе используемого агента и заголовков запроса, полученных от клиента.

Многие приложения и системы предоставляют интерфейс удаленного управления API, который может использоваться как для получения информации (например, для передачи данных в систему мониторинга), так и для управления этой системой через другое приложение или скрипты, которые чаще всего настраивают системные администраторы или специалисты в области DevOps.

Как дела в Windows

Большим преимуществом PowerShell (который по умолчанию используется в операционной системе Windows)перед интерпретатором Bash, это возможно конвертировать данные различных форматов без необходимости устанавливать дополнительные утилиты или библиотеки, а благодаря своему единому подходу к написанию скриптов, возможно фильтровать и управлять полученными данными с помощью объектной модели. Вот пример, который выведет запущенные процессы с фильтрацией по ключевому слову torrent в формате JSON:

$json = Get-Process *torrent* | Select-Object name,ws,cpu | ConvertTo-Json
$json
[
  {
    "Name": "qbittorrent",
    "WS": 57962496,
    "CPU": 12243.375
  },
  {
    "Name": "WebTorrent",
    "WS": 116465664,
    "CPU": 0.515625
  }
]

Если необходимо обработать данные, изначально полученные в формате JSON, используется команда ConvertFrom-Json . Полученные данные обрабатываются типовыми командами данного языка, и если это необходимо, конвертируются обратно в любой поддерживаемый формат:

$json | ConvertFrom-Json | Where-Object cpu -gt 1 | ConvertTo-Json
{
  "Name": "qbittorrent",
  "WS": 57962496,
  "CPU": 12243.375
}

Для поддержки остальных языков разметки можно установить соответствующий модуль с помощью команды Install-Module (например, PSYaml или PSToml), которые будет иметь точно такой же синтаксис.

Благодаря кроссплатформенной версии PowerShell Core, вы можете установить и использовать данный интерпретатор в системе Linux. Например, последняя версия 7.4.3 насчитывает 300 встроенных командлетов, а для конвертации данных из одного формата в другой не требуется изучение самого язык.

Список поддерживаемых команд по умолчанию, для конвертации данных в PowerShell

Список поддерживаемых команд по умолчанию, для конвертации данных в PowerShell

Примеры по работе с этими и другими командами вы можете найти в моем репозитории на GitHub. Если вы не хотите устанавливать и вызывать другой интерпретатор, а использовать только встроенный (чаще всего это Bash), потребуется установить в систему специальную утилиту, которая реализует данный механизм.

jqlang/jq

Самым популярным инструментом для обработки JSON данных является jq, который написан на языке C. Можно сказать, что это не просто утилита, а язык запросов, который позволяет выполнять сложные операции по извлечению, фильтрации и преобразованию данных.

Для установки в системе Ubuntu можно воспользоваться встроенным менеджером пакетов:

sudo apt install jq

Для начала определимся с шаблоном данных, который будет использоваться в дальнейшем. Для примера я возьму онлайн-инструмент Check-Host, который применяется для проверки доступности веб-сайтов и хостов, а главное, данный сервис предоставляет бесплатный API, так что попутно разберемся, как с ним работать.

Для начала, получим список доступных адресов нод (nodes), с которых возможно будет производить проверки любых хостов в Интернете, и передадим полученный вывод в команду jq:

nodes=$(curl -s -H "Accept: application/json" https://check-host.net/nodes/ips)
echo $nodes | jq

Список нод Check-Host до и после обработки с помощью jq

Список нод Check-Host до и после обработки с помощью jq

Как видно из примера на скриншоте, после обработки входных данных командой jq вывод отображается в правильно структурированном формате, а все элементы подсвечиваются соответствующим цветом.

Если необходимо получить количество дочерних элементов выбранного блока (в примере, nodes), используется функция length через pipe (|) внутри запроса, который заключен в кавычки:

echo $nodes | jq '.nodes | length'
43

Если обратиться к дочерним элементам блока .nodes, используя в запросе .nodes[], то получим массив цифр, каждая из которых будет составлять длину элемента в массиве (количество букв и символов в строке). Тем самым, обращаясь к дочерним элементам по имени, мы фильтруем полученный вывод, например, что бы получить содержимое выбранного элемента в массиве nodes[], обратимся к нему по его порядковому номеру индекса (отчет начинается с 0):

echo $nodes | jq .nodes[0]
"185.143.223.66"

echo $nodes | jq .nodes[1]
"38.145.202.12"

echo $nodes | jq -r .nodes[1]
38.145.202.12

echo $nodes | jq -r .nodes[-1]
65.109.182.130

Ключ -r используется для вывода в формате строки (raw string).

Для каждой ноды можно получить подробную информацию, сделаем это с помощью нового запроса к API:

hosts=$(curl -s -H "Accept: application/json" https://check-host.net/nodes/hosts)

Теперь, при обращении к блоку .nodes вместо ip-адресов, мы получим имена хостов, для каждого из которых есть вложенные элементы. Так как .nodes теперь не является массивом, а представляет из себя формат объекта, где каждый отдельный элемент это набор пар: ключ-значение. Что бы получить список этих ключей, используем функцию to_entries[] и обращаемся к свойству key:

echo $hosts | jq -r '.nodes | to_entries[].key'
bg1.node.check-host.net
br1.node.check-host.net
ch1.node.check-host.net
...

echo $hosts | jq -r '.nodes | to_entries[0].key'
bg1.node.check-host.net

Если необходимо получить только значение, обращаемся к свойству value:

echo $hosts | jq -r '.nodes | to_entries[0].value'
{
  "asn": "AS9028",
  "ip": "93.123.16.89",
  "location": [
    "bg",
    "Bulgaria",
    "Sofia"
  ]
}

Тем самым каждый элемент объекта содержит 3 свойства, одно из которых является массивом (location). Такой же результат можно получить, обратившись на прямую к элементу объекта по имени ключа:

echo $hosts | jq '.nodes."bg1.node.check-host.net"'
{
  "asn": "AS9028",
  "ip": "93.123.16.89",
  "location": [
    "bg",
    "Bulgaria",
    "Sofia"
  ]
}

Что бы получить все значения из последнего объекта, сначала преобразуем отдельные объекты внутри nodes в массив, и передаем полученный вывод в функцию last:

echo $hosts | jq '.nodes | [.[]] | last'
{
  "asn": "AS207713",
  "ip": "185.143.223.66",
  "location": [
    "us",
    "USA",
    "Atlanta"
  ]
}

Можно произвести проверку содержимого элементов, например, первое значение массива location содержит код страны в формате ISO 3166.

echo $hosts | jq '.nodes | to_entries[].value.location[0] == "ru"'

Такая конструкция вернет результат для каждого элемента в виде массива строк, в каждой из которых будет true, если содержимое элемента равно ru или false, если условие ложно.

Теперь произведем выборку данных. Вначале заберем все ключи и значения объектов из элемента .nodes, затем передадим значения дочерних элементов в ключи с новыми названиями, тем самым пересоберем массив объектов:

echo $hosts | jq '.nodes | to_entries[] | {
  Host: .key,
  Country: .value.location[1],
  City: .value.location[2]
}'

{
  "Host": "bg1.node.check-host.net",
  "Country": "Bulgaria",
  "City": "Sofia"
}
{
  "Host": "br1.node.check-host.net",
  "Country": "Brazil",
  "City": "Sao Paulo"
}
{
  "Host": "ch1.node.check-host.net",
  "Country": "Switzerland",
  "City": "Zurich"
}
...

Если необходимо получить вывод в формате текста, т.е. массив из строк, то в теле запроса формируем строку, в которой для получения содержимого элементов каждого из объектов будут использоваться скобки, перед которыми находится знак \, а произвольные символы, которые необходимо добавить, буду располагаться за пределами этих скобок:

echo $hosts | jq -r '.nodes | to_entries[] | "\(.key) (\(.value.location[1]), \(.value.location[2]))"'
bg1.node.check-host.net (Bulgaria, Sofia)
br1.node.check-host.net (Brazil, Sao Paulo)
ch1.node.check-host.net (Switzerland, Zurich)
...

Что бы передать внешнюю переменную, которая будет использоваться внутри запроса, используется параметр --arg:

echo $hosts | jq --arg v "$var" -r '.nodes | to_entries[] | "\(.key) \($v) \(.value.location[1]) \($v) \(.value.location[2])"'
bg1.node.check-host.net - Bulgaria - Sofia
br1.node.check-host.net - Brazil - Sao Paulo
ch1.node.check-host.net - Switzerland - Zurich
...

Для того, что бы получить только нужные объекты, необходимо произвести фильтрацию с помощью select . Например, выведем список хостов, которые в первом значение массива location содержат ключевое слово ru:

echo $hosts | jq -r '.nodes | to_entries[] | select(.value.location[0] == "ru") | .key'
ru1.node.check-host.net
ru2.node.check-host.net
ru3.node.check-host.net
ru4.node.check-host.net

Всего имеется 4 ноды в регионе ru. Произведем обратный процесс, отфильтруем объекты, которые не содержат (!=) указанное значение, и получим общее количество найденных элементов:

echo $hosts | jq '.nodes | length'
43
echo $hosts | jq '.nodes | to_entries | map(select(.value.location[0] != "ru")) | length'
39

Функция map() создает массив только из тех объектов, которые соответствуют условию, тем самым исходные отдельные объекты формата {} {} группируются в один массив формата [{},{}].

Возможно также использовать сразу несколько условий с помощью and и or:

echo $hosts | jq -r '.nodes | to_entries[] | select(.value.location[0] == "ru" or .value.location[0] == "tr") | .key'
ru1.node.check-host.net
ru2.node.check-host.net
ru3.node.check-host.net
ru4.node.check-host.net
tr1.node.check-host.net
tr2.node.check-host.net

Важный момент, если мы проверяем тип данных string (строка), которая по умолчанию заключена в кавычки, то мы их используем и в ключевом слове (как в примере выше, "ru" или "tr"), но, если значение содержит тип int (целое число), то кавычки применяться не будут, это также можно увидеть в исходном выводе синтаксиса JSON:

echo '{"type_string": "string", "type_int": 123}' | jq
{
  "type_string": "string",
  "type_int": 123
}

В примере, содержимое элемента type_int не содержит кавычек.

Если же необходимо отфильтровать объекты по частичному совпадению содержимого, то искомое слово помещается в функцию index() . Например, выведем список хостов, которые в названии ключа содержат ключевое слово jp (регион Japan):

echo $hosts | jq -r '.nodes | to_entries[] | select(.key | index("jp")) | .key'
jp1.node.check-host.net

Теперь произведем icmp проверку любого публичного ресурса в Интернете. Передаем в url запроса параметры адрес хоста host=yandex.ru и количество нод max_nodes=3, которые будут использоваться для проверки. Первым запросом мы запускаем проверку и забираем ее id c помощью jq, после чего по этому id получаем результат проверки.

host="yandex.ru"
protocol="ping"
# Забрать id для получения результатов
check_id=$(curl -s -H "Accept: application/json" "https://check-host.net/check-$protocol?host=$host&max_nodes=3" | jq -r .request_id)
# Функция получения результатов проверки по id
function check-result {
    curl -s -H "Accept: application/json" https://check-host.net/check-result/$1 | jq .
}
# Получить суммарное количество хостов, с которых производится проверка
hosts_length=$(check-result $check_id | jq length)
while true; do
    check_result=$(check-result $check_id)
    # Забираем результат и проверем, что содержимое всех проверок не равны null
    check_values_not_null=$(echo $check_result | jq -e 'to_entries | map(select(.value != null)) | length')
    if [[ $check_values_not_null == $hosts_length ]]; then
        echo $check_result | jq
        break
    fi
    sleep 1
done

Так как проверка занимает какое-то время, если сразу попытаться получить результат, нам вернется только список нод с которых запущена проверка, но их значения будут равны null. По этому, вначале фиксируем суммарное количество нод (хотя это делать не обязательно, так как мы задаем его в параметре), и в цикле while сопоставляем это количество (hosts_length) с тем количеством, у которых вывод (.value) не равен null.

Результат проверки хоста yandex.ru с помощью трех нод

Результат проверки хоста yandex.ru с помощью трех нод

В данном случае три хоста из разных уголков земли отправили по 4 пакета с помощью команды ping на указанный нами хост в запросе. Если необходимо произвести проверку другого протокола, то для удобства в скрипте выше я добавил две переменные, где можно указать другой протокол (http, tcp, udp или dns). Например, проверим доступность TCP порта 443:

host="yandex.ru:443"
protocol="tcp" # udp/http/dns

Результат проверки TCP порта 443

Результат проверки TCP порта 443

Как видно из примера, хост ua2.node.check-host.net не смог получить доступ к указанному порту, вернув ошибку: Connection refused.

Также возможно указать, с каких именно хостов (из разных регионов) производить проверку, используя соответствующий параметр, например, добавить в конец url-запроса: &node=ru4.node.check-host.net. Полную версию скрипта с обработкой входных параметров для Bash и PowerShell вы можете найти в исходном репозитории на GitHub. Далее, используя утилиту jq можно обработать эти данные, и например, настроить вывод в систему мониторинга.

Функций у данной утилиты достаточно много, например, с помощью следующей конструкции можно получить ГБ из байт и округлить вывод до 2 символом после запятой:

echo '{
"iso":
    [
        {"name": "Ubuntu", "size": 4253212899},
        {"name": "Debian", "size": 3221225472}
    ]
}' | jq '.iso[] | {name: .name, size: (.size / 1024 / 1024 / 1024 | tonumber * 100 | floor / 100 | tostring + " GB")}'

{
  "name": "Ubuntu",
  "size": "3.96 GB"
}
{
  "name": "Debian",
  "size": "3 GB"
}

Или получить процент из дробной чисти:

echo '{
"iso":
	[
		{
			"name": "Ubuntu",
			"progress": 0.333
		}
	]
}' | jq '.iso[] | {name: .name, progress: (.progress * 100 | floor / 100 * 100 | tostring + " %")}'

{
  "name": "Ubuntu",
  "progress": "33 %"
}

Хотя возможностей у данного языка запросов очень много, приведенных мною пример будет достаточно для решения большинства задач. Хочу заметить, так как JSON представляет из себя объектную модель, в связке с Bash способность хранить и обрабатывать данные очень сильно расширяют возможности при написании скриптов, даже если вы не работаете с API, а главное, такой подход куда надежнее по сравнению с классической обработкой строк.

Другие инструменты

У jq существует много поклонников, по мимо обширного сообщества которое принимают участие в разработке, исправлении багов, а также оказывают поддержку в разделе issues на GitHub, присутствуют и интерфейсы командной строки, которые упрощают работу с данной утилитой, например jid и jqp.

Первый инструмент позволяет интерактивно взаимодействовать с данными, где помимо автоматической подстановки значений с помощью кнопки Tab, вы можете получить список всех ключей в формате выпадающего списка и переключаться между ними стрелочками, при этом сразу наблюдая результат на экране. Хотя инструмент предназначен в первую очередь для удобного и быстрого просмотра JSON данных в вашей консоли, такие запросы в дальнейшем можно применять в jq. Второй инструмент похож на первый, но использует две панели, в левой отображается исходный документ, а в правой отфильтрованный с помощью введённого вами запроса, который можно скопировать с помощью комбинации клавиш Ctrl+Y .

Интерактивное взаимодействие с jq через jqp

Интерактивное взаимодействие с jq через jqp

Также, существует и несколько других утилит, например, dasel, который написан на современном и быстром GoLang. По мимо обработки JSON, он также поддерживает YAML, TOML, XML и CSV. Базовый синтаксис при обращении к элементам объектов очень похож на jq, и имеет единый формат для всех языков разметки.

curl -s https://check-host.net/nodes/ips | dasel -r json '.nodes.[0]'
"185.86.77.126"

По мимо этого, dasel способен конвертировать данные между языками:

# Конвертируем JSON в YAML
echo '{"name": "Tom"}' | dasel -r json -w yaml
name: Tom

# Конвертируем JSON в MXL
echo '{"name": "Tom"}' | dasel -r json -w xml
Tom

Для конвертации данных из одного формата в другой еще можно воспользоваться утилитой sttr. Пример для YAML:

Конвертация JSON в YAML и наоборот.

Конвертация JSON в YAML и наоборот.

По мимо конвертации, данный инструмент предоставляет очень много возможностей для работы со строкой, например, возможно кодировать и декодировать HEX или Base64, менять регистр, сортировать, извлекать уникальные строки и многое другое.

Если вам требуется проверить синтаксис входных данных в формате JSON, можете воспользоваться достаточно старым инструментом jsonlint, который по сегодняшний день отлично с этим справляется. Для работы данного инструмента потребуется установить в системе Node.js и с помощью менеджера пакетов npm установить данную утилиту:

apt-get install -y nodejs
npm install jsonlint -g

Утилита подскажет, где была допущена ошибка, а в случае корректного чтения входных данных, выведет содержимое JSON на экран:

Работа с jsonlint

Работа с jsonlint

Также возможно использовать встроенные возможности большинства IDE, например, VSCode, который способен показать, где именно у вас допущена ошибка и форматировать документ.

VSCode подсказывает, что в 12 строке пропущена запятая

VSCode подсказывает, что в 12 строке пропущена запятая

Итог

Хотя это далеко не все инструменты и на просторах GitHub можно найти несколько десятков, достаточно выбрать один, который будет удовлетворять вашим потребностям и привыкнуть к его синтаксису. Также существует много источников с заметками по разным утилитам Linux, например, manned, где присутствует документация для jq, и за время практики у меня тоже их накопилось немало, по этому решил опубликовать их в репозитории на GitHub до кучи к заметкам по PowerShell (предварительно оформив документ в формате Markdown), а также добавил в веб-версию, где присутствует удобный поисковик. Так как многие команды и ключи забывается когда с ними регулярно не работаешь, по этому такие заметки я части использую с рабочего компьютера, и меня такой подход выручает.

© Habrahabr.ru