Статьи

Bash That JSON (с JQ)

Понятие SaaS может мешать операциям людей; Я говорю это как операционный человек сам. Вера просто не приходит к нам естественным образом, поэтому не всегда легко доверить ту или иную часть инфраструктуры поставщику программного обеспечения как услуги. Однако я считаю справедливым сказать, что все большее число из нас приходит к пониманию того, что наша личная способность соединять кабель rj45 или устанавливать MySQL не является фактором, который делает наши продукты или услуги лучше, чем у наших конкурентов. По мере расширения нашего самосознания — когда мы позволяем себе осознать, какой из наших вкладов приводит к конкурентным преимуществам — мы, естественно, начинаем стремиться оптимизировать нашу рабочую нагрузку для этих видов деятельности.

Самосознание помогает нам определить виды деятельности, на которые мы не должны тратить время, те части инфраструктуры, которые мы можем позволить себе переложить на SaaS. И по мере того, как происходит этот переход — когда мы передаем части нашей инфраструктуры монстру SaaS, — наши интерфейсы меняются с инструментов оболочки на API. 

Shell — это фантастический язык автоматизации с наименьшим общим знаменателем для работы с почтенными инструментами оболочки, с которыми мы все выросли. Да, это некрасиво и утилитарно. Да, ему не хватает базовых функций, присутствующих в каждом «серьезном» языке программирования. И да, иногда это неразумно используется для решения проблем, которые он не в состоянии решить. Но этот удобный маленький множитель силы всегда рядом с вами, позволяя быстро и легко склеивать одинокие маленькие инструменты, превращая их в многократно используемые решения, которые экономят время и головные боли.

Поскольку все больше вещей, на которые я полагаюсь каждый день, переходят на SaaS, я обнаружил, что трачу меньше времени на склеивание инструментов оболочки и больше времени на склеивание API. Однако для склейки API требуется работа с JSON. Разбор, извлечение, преобразование; JSON везде — вездесущий. Неизбежные. И у Shell просто не было очень хорошего ответа на вопрос JSON.

Пока, то есть JQ.

Представляя JQ

jq  — это быстрый, легкий, гибкий процессор CLI JSON. jq stream обрабатывает JSON, как awk stream обрабатывает текст. jq, в сочетании с cURL, предлагает мне написать оболочку для склеивания веб-API, что довольно здорово. Это помогло мне написать  shellbrato , библиотеку оболочки для API Librato, а также множество других небольших инструментов, которые я использую изо дня в день для таких вещей, как поиск PR, назначенных мне через API GitHub, и преобразование тегов экземпляра AWS в IP через API AWS ,

Давайте вместе попробуем jq, используя его для проверки большого неизвестного большого двоичного объекта JSON. Следующая команда возьмет BLOB-объект JSON, представляющий открытые проблемы в общедоступном репозитории Docker GitHub, и сохранит его в переменной оболочки с именем foo:

foo=$(curl 'https://gist.githubusercontent.com/djosephsen/a1a290366b569c5b98e9/raw/c0d01a18e16ba7c75d31a9893dd7fa1b8486a963/docker_issues')

Если вы откроете $ {foo}, вы, вероятно, увидите большой непонятный фрагмент текста (если только Докеру не удастся закрыть все их открытые проблемы к тому времени, как вы прочитаете это). Вы можете использовать jq, чтобы переформатировать этот текст и сделать его более читабельным, например:

echo ${foo} | jq .

Первый аргумент jq — это «фильтр». Точка, пожалуй, самый простой из всех фильтров jq. Это соответствует текущему вводу. Вы можете думать о точке как о бесконечно плотной частице JSON. Каждый раз, когда вы видите начальную точку (то есть точку, перед которой ничего нет), вы смотрите на все тело ввода, погруженное в маленькую точку. 

Существует множество jq-фильтров, и на первый взгляд многие из них покажутся глупыми и бесполезными, но, как вы обнаружите на своем пути к jq adeptness, они объединяются удивительным образом. Ключевое слово «тип» — это фильтр jq, который для каждого объекта на входе выдает тип этого объекта. Например:

echo '[][]{}' | jq type

дает «массив» «массив» «объект» из фильтра типов. Ключевое слово length генерирует размер (количество элементов) каждого объекта на входе.

Итак, повторно используя предыдущий пример:

echo '[][]{}' | jq length

дает три 0, поскольку оба массива и объект пусты. Тип и длина действительно полезны в контексте проверки больших двоичных объектов JSON, которые мы никогда раньше не видели. Запятый фильтр, который копирует входные данные и последовательно передает их в каждый окружающий его фильтр, позволяет нам использовать тип и длину одновременно. Попробуйте это на нашем огромном блобе:

echo ${foo} | jq 'type,length'

Для меня это приводит к «массиву» и 30. Запятая подала копию ввода, чтобы сначала ввести тип, а затем длину. В jq мы также можем направить вывод одного фильтра на вход следующего. Итак, более явный способ сделать то же самое, что мы только что сделали:

echo ${foo} | jq '.|type,length'

Это дает тот же результат, но дает вам лучшее представление о том, как фильтры работают вместе; возьмите ввод (точка) и передайте его фильтру запятых, который копирует ввод и отправляет копию в тип, а затем в длину. Итак, мы имеем дело с одним массивом, который содержит 30 элементов. 

Давайте посмотрим внутрь. Мы можем использовать квадратные скобки, чтобы развернуть слои ввода. Я думаю о них как о «неаккуратных скобках», когда вижу их в jq:

echo ${foo} | jq '.[]'

Это дает нам необработанное содержимое массива (обратите внимание: по сравнению с простым jq ‘. Вывод больше не заключен в квадратные скобки). Это не очень полезно, так как контента много, так что это прокручивается за пределы экрана. Давайте попробуем использовать тип и длину для развернутого ввода:

echo ${foo} | jq '.[] | type,length'

Что ж, похоже, у нас есть куча объектов различной длины (мы уже знаем, что их 30), но трудно точно сказать, сколько их, так как они также прокручиваются с моего экрана. 

Давайте добавим еще несколько почтенных инструментов оболочки, чтобы помочь нам интерпретировать этот вывод:

echo ${foo} | jq '.[] | type,length' | sort | uniq -c

Теперь лучше, мы видим 30 объектов. В моем конкретном входе 20 из объектов имеют 19 атрибутов, и 10 из них имеют 20 атрибутов. Это странно: интересно, в чем разница между этими двумя видами. Фильтр ‘keys’ вернет массив имен атрибутов для каждого объекта на входе:

echo ${foo} | jq '.[]|keys'

Это показывает нам кучу атрибутов, но чтобы понять разницу между двумя типами объектов, мы должны вернуть sort и uniq:

echo ${foo} | jq '.[]|keys' | sort | uniq -c

А-а-а, в этом выводе я вижу, что только 10 моих объектов имеют атрибут pull_request. Это имеет смысл, так как не каждая проблема GitHub будет иметь соответствующий pull_request. 

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

echo ${foo} | jq '.[0]'

Или только ключи первого выпуска:

echo ${foo} | jq '.[0] | keys'

Или просто первый ключ первого номера

echo ${foo} | jq '.[0] | keys | .[0]'

Если мы просто хотели список идентификаторов проблем:

echo ${foo} | jq '.[].id'

Как бы мы выбрали конкретную проблему по ее идентификационному номеру? Фильтр select — это первый фильтр, который мы будем использовать, который принимает аргумент. Он выглядит как функция C, и ему предназначено дать выражение, которое возвращает «true» или «false». На практике jq интерпретирует ненулевые значения как «true», поэтому вы также можете передать выражения «select», которые возвращают числовые значения. У jq есть целый ряд операторов равенства, которые вы ожидаете. Таким образом, мы можем выбрать выпуск 117446711 с помощью:

echo ${foo} | jq '.[] | select(.id==117446711)'

Давайте поговорим немного больше о том, как это работает. Функция «select» для каждого объекта на своем входе, если ее аргументное выражение возвращает true для этого объекта, возвращает этот объект без изменений. Если его аргумент возвращает false, «select» ничего не выводит. 

Скобки с аргументами в jq немного похожи на Лас-Вегас: все, что там происходит, остается там. Я имею в виду, что вы можете выполнять все виды преобразования входных данных внутри аргумента «select», но «select» будет по-прежнему выводить исходный ввод без изменений. Например, скажем, мы хотели выбрать каждую проблему с одной или несколькими метками:

echo ${foo} | jq '.[] | select((.labels|length)>=1)'

Внутри этого фильтра «select» мы преобразуем входной объект, отфильтровывая только массив меток, а затем передаем массив меток в фильтр длины, чтобы увидеть, насколько он велик. Если оно больше или равно 1, то выражение выходит из «true», и «select» повторяет исходный объект (а не тот, который мы искали в процессе подсчета размера массива меток).

Последнее, что я хочу показать вам, — это несколько фильтров, которые вы можете использовать для проверки наличия ключей или значений внутри объекта. Я часто использую два «has» и «index». Прежний из этих фильтров выходит с логическим значением «истина» или «ложь», а другой с порядковым числом. Они оба идеально подходят для вложения внутри функции select () следующим образом:

echo ${foo} | jq '.[] | select (.| has("pull_request"))'

‘has’, как вы, наверное, догадались, проверяет наличие именованного ключа во входном объекте. Если ключ существует, «имеет» выход истины, в противном случае он выходит ложь. В приведенной выше команде мы передали копию ввода «select» в «has», чтобы проверить наличие атрибута «pull_request». Каждая проблема, имеющая атрибут, заставит ‘has’ выйти из ‘true’, что, в свою очередь, заставит ‘select’ вывести объект проблемы. В противном случае, «выберите» будет съесть объект. Это довольно типичный способ разбора только тех записей, которые имеют определенный ключ. 

‘index’ проверяет значения. Более конкретно, он возвращает значение индекса данного аргумента в своем вводе. Если вы дадите индексу массив из трех значений и запросите второе значение следующим образом:

echo '["foo","bar","bash"]' | jq 'index("bar")'

‘index’ вернет 1 (массивы индексируются нулем в jq). Если вы передадите ему строку и запросите подстроку, index вернет значение индекса символа в строке, где начинается подстрока. Например:

echo '"foo"' | jq 'index("oo")'

… также возвращает 1. ‘index’ возвращает ноль, если не может найти значение, которое вы ищете во входных данных. Мы можем вложить «index» внутрь «select» явно так:

echo ${foo} | jq '.[] | select((.state|index("open")>=0))'

Буквально, если значение индекса внутри меток текущей записи для значения «open» больше нуля, выберите запись. К счастью, фильтр «select» интерпретирует числовой вывод как «true», а нулевой вывод как «false», поэтому нам не нужно быть явным, и мы могли бы переписать эту последнюю команду как:

echo ${foo} | jq '.[] | select(.state|index("open"))'

Это больше похоже на наш «has», который проверяет наличие или ключи. С «index» и «has», вложенными в «select», у вас есть около 80% того, что вам нужно, чтобы манипулировать структурами JSON в оболочке для развлечения и получения прибыли. Фактически, большинство инструментов запросов, которые я написал для решения таких задач, как определение IP-адресов экземпляра AWS из имен тегов, используют только то, что я уже рассмотрел. Отсюда я покажу вам конструкцию объекта (которую я считаю скобками) и отображение, но эти два предмета действительно требуют отдельной статьи.

Отправьте нам твит на  @librato,  если вы хотите увидеть эту статью, и удачи вам в JSON!