jsqry — лучше, чем jq

в 12:41, , рубрики: cli, ECMAScript, javascript, jq, json, node.js, nodejs, Query, quickjs

В своей прошлой статье на Хабре я писал про библиотеку Jsqry, которая предоставляет простой и удобный язык запросов (DSL) к объектам JSON. С тех пор прошло много времени и библиотека тоже получила свое развитие. Отдельный повод для гордости — библиотека имеет 98% покрытие кода тестами. Однако в этой статье речь не совсем о ней.

Думаю, многие из вас знакомы с инструментом jq, который является практически стандартом де-факто для работы с JSON в командной строке и скриптах. Я тоже являлся её активным пользователем. Но меня все время беспокоила неоправданная сложность и неинтуитивность синтаксиса запросов этой утилиты. И не меня одного, вот лишь несколько цитат с hacker news:

I have been using jq for years and still can't get it to work quite how I would expect it to.

I have the same issue with jq. I need to use my google fu to figure out how to do anything more than a simple select.

I don't know what the term would be, mental model, but I just can't get jq to click. Mostly because i only need it every once in a while. It's frustrating for me because it seems quite powerful.

I know I might be a dissenting opinion here, but I can never wrap my head around jq. I can manage jq ., jq .foo and jq -r, but beyond that, the DSL is just opaque to me.

Let's just say it: jq is an amazing tool, but the DSL is just bad.

Yeah, I find jq similar to writing regexes: I always have to look up the syntax, only get it working after some confusion why my patterns aren't matching, then forget it all in a few days so have to relearn it again later.

Одним словом, вы уже наверное догадались. Пришла идея, а почему бы не обратить мою JS библиотеку в исполняемый файл для командной строки. Здесь есть один нюанс. Библиотека написана на JS и её DSL также опирается на JS. Это значит, что надо найти способ упаковать программу и какой-нибудь JS-runtime в самодостаточный исполняемый файл.

jsqry — GraalVM edition

Для тех кто еще не в теме (неужели еще есть такие? oO) напомню, что GraalVM это такая прокачанная JVM от Oracle с дополнительными возможностями, самые заметные из которых:

  1. Полиглотная JVM — возможность бесшовного совместного запуска Java, Javascript, Python, Ruby, R, и т.д. кода
  2. Поддержка AOT-компиляции — компиляция Java прямо в нативный бинарник
  3. Улучшения в JIT-компиляторе Java.

Освежить представление о Graal можно, например, в этой хабра-статье.

Теоретически, объединение возможностей пунктов 1. и 2. должно решить поставленную задачу — обратить код на JS в исполняемый файл.

Так родился проект https://github.com/jsqry/jsqry-cli. Правда, не спешите добавлять в закладки — в данный момент проект deprecated. Идея оказалась рабочей, но непрактичной. Дело в том, что размер исполняемого файла получался 99 Мб. Как-то не очень хорошо для простой утилиты командной строки. Тем более, если сравнить с jq с её размером 3.7 Мб для последней версии для Linux 64.

В идеале хотелось бы получить размер не больше мегабайта.

Тем не менее, решил оставить этот репозиторий как практический пример того как собрать из Java + JS кода исполняемый файл при помощи GraalVM.

Небольшой обзор этого решения

Решение имеет основной код запускаемого приложения в единственном файле App.java. Этот код выполняет обработку параметров командной строки, используя стандартную java-библиотеку Apache Commons CLI.

Далее java-код вызывает код на javascript из файлов, находящихся в директории ресурсов src/main/resources.

При этом интересный момент. Вроде-бы простейший код для вычитывания содержимого файла из ресурса

scripts.add(new String(Files.readAllBytes(Paths.get(jsFileResource.toURI()))));

под Граалем (то есть, будучи скомпилированным через native-image) падало с

java.nio.file.FileSystemNotFoundException: Provider "resource" not installed

Выручил древний "хак" для чтения строки из InputStream

scripts.add(new Scanner(jsFileResource.openStream()).useDelimiter("\A").next());

Короче говоря, надеяться на 100% поддержку всех функций стандартной Java Граалем все еще не приходится.

Недавно аналогичной неприятной находкой оказалось отсутствие поддержки java.awt.Graphics. Это помешало использовать GraalVM для реализации AWS Lambda для конвертации картинок.

jsqry — QuickJS edition

Где-то в это же время я узнал о новом компактном движке JS QuickJS от гениального французского программиста Фабриса Беллара. В своем составе этот инструмент несёт компилятор qjsc джаваскрипта в исполняемый файл. Также поддерживается почти полная совместимость с ES2020. То что нужно!

Таким образом, появилась вторая инкарнация CLI-версии jsqry: https://github.com/jsqry/jsqry-cli2.
Этот подход оказался более жизнеспособным и уже принес несколько релизов.

Итак, что же такое jsqry?

jsqry это маленькая утилита командной строки (похожая на jq) для выполнения запросов к JSON используя "человеческий" DSL.

Цель этой разработки — представить функционал JS библиотеки jsqry в форме интерфейса командной строки.

Примеры использования

запрос

$ echo '[{"name":"John","age":30},
         {"name":"Alice","age":25},
         {"name":"Bob","age":50}]' | jsqry 'name'
[
  "John",
  "Alice",
  "Bob"
]

первый элемент

$ echo '[{"name":"John","age":30},
         {"name":"Alice","age":25},
         {"name":"Bob","age":50}]' | jsqry -1 'name'
"John"

использование параметризации запроса

$ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' 
    | jsqry '[ _.age>=? && _.name.toLowerCase().startsWith(?) ]' --arg 30 --arg-str joh 
[
  {
    "name": "John",
    "age": 30
  }
]

использование в роли простого JSON pretty-printer

$ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' 
 | jsqry
[
  {
    "name": "John",
    "age": 30
  },
  {
    "name": "Alice",
    "age": 25
  },
  {
    "name": "Bob",
    "age": 50
  }
]

Выходной JSON утилиты по умолчанию отформатирован. И раскрашен!

что-то более хитрое

Отфильтровать элементы больше 2, добавить к каждому 100, отсортировать по убыванию и взять 2 последних элемента. Комбинируя эти возможности вы можете строить сколь угодно сложные запросы. Узнать больше о поддерживаемом DSL.

$ echo '[1,2,3,4,5]' | jsqry '[_>2] {_+100} s(-_) [-2:]'
[
  104,
  103
]

полная мощь JS

Поскольку jsqry вмещает полноценный JS-движок в исполняемом файле менее 1 Мб, полная мощь JS в ваших руках!

$ echo '["HTTP://EXAMPLE.COM/123", 
         "https://www.Google.com/search?q=test", 
         "https://www.YouTube.com/watch?v=_OBlgSz8sSM"]' 
 | jsqry '{ _.match(/://([^/]+)//)[1].toLowerCase() }'
[
  "example.com",
  "www.google.com",
  "www.youtube.com"
]

help-сообщение

$ jsqry
jsqry ver. 0.1.2
Usage: echo $JSON | jsqry 'query'
 -1,--first     return first result element
 -h,--help      print help and exit
 -v,--version   print version and exit
 -c,--compact   compact output (no pretty-print)
 -u,--unquote   unquote output string(s)
 -as ARG,
 --arg-str ARG  supply string query argument
 -a ARG,
 --arg ARG      supply query argument of any other type

Небольшое сравнение с jq

А здесь я подготовил небольшое практическое сравнение jq и jsqry на примерах.

Установка

Текущая версия (на момент написания): 0.1.2.

К сожалению, только Linux x64 поддерживается в данный момент. Надеюсь, поддержка других платформ будет скоро добавлена. Буду рад здесь вашей помощи.

Чтобы установить или обновить утилиту, просто выполните в командной строке приведенную ниже команду:

$ sudo bash -e -c "
wget https://github.com/jsqry/jsqry-cli2/releases/download/v0.1.2/jsqry-linux-amd64 -O/usr/local/bin/jsqry
chmod +x /usr/local/bin/jsqry
echo "jsqry $(jsqry -v) installed successfully" 
"

О тестировании CLI-утилиты

При разработке утилиты на GitHub хотелось реализовать какое-то подобие автоматического тестирования. Юнит-тесты довольно просто писать, когда вы работаете на уровне языка программирования. Интереснее дело обстоит, если хочется протестировать CLI-утилиту как единое целое, как черный ящик. Благо, в нашем случае это должно быть просто и логично, поскольку утилита представляет собой то, что функциональщики бы назвали чистой функцией — выход определяется исключительно входом.

Попытав Гугл запросами вида "bash unit testing" и отметя варианты BATS, ShellSpec, Bach и несколько других подходов, как чересчур тяжеловесные для моего случая, а также самописную систему тестирования (картинка про 14 стандартов), остановился на решении tush, гениальном в своей простоте.

Тесты на tush представляют собой текстовый файл в таком синтаксисе

$ command --that --should --execute correctly
| expected stdout output

$ command --that --will --cause error
@ expected stderr output
? expected-exit-code

Причем tush разбирает только строки начинающиеся на $, |, @ и ? — все остальные могут быть любым текстом, например описанием соответствующих тестов. При запуске теста инструмент запускает все строки, начинающиеся на $ и просто сравнивает реальный вывод с ожидаемым, используя обычный diff. В случае отличия тест заканчивается неудачей, а diff отличия выводится пользователю. Пример:

$ /bin/bash /home/xonix/proj/jsqry-cli2/tests.sh
--- tests.tush expected
+++ tests.tush actual
@@ -1,5 +1,5 @@
 $ jsqry -v
-| 0.1.2
+| 0.1.1

 $ jsqry -h
 | jsqry ver. 0.1.1
!!! TESTS FAILED !!!

Таким образом удалось покрыть тестами базовые сценарии работы с утилитой в виде одного файла
tests.tush.

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

Удалось этот тестовый сценарий реализовать в виде GitHub Action, который запускается на каждый коммит, гарантируя корректность каждого изменения и предоставляя замечательный бейдж:

Build and test

Другие особенности решения

Раскрашивание JSON

Добавить раскраску выходного JSON оказалось на удивление просто. Решение основано на подходе из проекта zvakanaka/color-json с немного оптимизированными цветами, которые были подобраны на основе прекраснейшего StackOverflow комментария. Для примера привожу сравнение раскраски с jq. В моей версии строки более яркие, а null имеет красный цвет для пущей заметности.

screenshot

Понятно, что раскрашивание будет выполняться только при работе с терминалом.

Подключение npm-версии библиотеки в QuickJS

Опишу немного процесс по которому npm-версия библиотеки jsqry подтягивается и включается в результирующий исполняемый файл. Для этого присутствует стандартный package.json с необходимой версией библиотеки. Библиотека вытягивается стандартным npm i. Затем используется небольшой скрипт prepare-for-qjs.py, роль которого состоит в замене экспортирования в стиле nodejs на экспортирование в стиле модулей ES, только последнее поддерживается движком QuickJS. Далее уже полученный файл импортируется в основной код утилиты jsqry-cli.js.

Чтение входной строки в UTF-8 в QuickJS

В случае QuickJS некоторой мороки стоит считывание строки из stdin. Дело в том, что минималистичная стандартная библиотека, доступная в QuickJS, реализует только считывание байтов. Поэтому понадобился некоторый ручной код, чтоб перегнать байтики UTF-8 в JS-строку. К счастью, его не пришлось изобретать, а удалось позаимствовать из другого проекта на QuickJS: twardoch/svgop.

Сборка утилиты

Сборка утилиты выполняется скриптом build.sh.

Перечислю несколько "фишек" этого скрипта, которые оказались весьма полезными.

Первое — скрипт безусловно вызывает в самом конце скрипт тестов tests.sh. Это гарантирует, что каждая вновь собранная версия утилиты будет протестирована, а сборка развалится если тесты будут неудачны.

Второе — скрипт build.sh автоматически скачивает и компилирует заданную версию QuickJS, а скрипт tests.sh выполняет то же для инструмента тестирования tush. Очень удобно — можно мгновенно продолжить разработку проекта на другой машине без лишних телодвижений.

Третий момент. В самом конце сборки выполняется команда ls -lh jsqry чтоб показать размер результирующего файла. Как я уже упомянул, я немного параноидален на этот счет, хочется чтоб CLI-утилита имела наименьший размер. Я рад, что это принесло полезный побочный результат — помогло устранить регрессию, выявленную этой проверкой в одном из прошлых релизов QuickJS.

В данный момент размер исполняемого файла составляет 652 KB. Мне кажется, довольно неплохо, учитывая что файл включает в себя полноценный движок современного стандарта JS.

В качестве послесловия

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

Автор: Владимир Губарьков

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js