В своей прошлой статье на Хабре я писал про библиотеку 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 aroundjq
. I can managejq .
,jq .foo
andjq -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 с дополнительными возможностями, самые заметные из которых:
- Полиглотная JVM — возможность бесшовного совместного запуска Java, Javascript, Python, Ruby, R, и т.д. кода
- Поддержка AOT-компиляции — компиляция Java прямо в нативный бинарник
- Улучшения в 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, который запускается на каждый коммит, гарантируя корректность каждого изменения и предоставляя замечательный бейдж:
Другие особенности решения
Раскрашивание JSON
Добавить раскраску выходного JSON оказалось на удивление просто. Решение основано на подходе из проекта zvakanaka/color-json с немного оптимизированными цветами, которые были подобраны на основе прекраснейшего StackOverflow комментария. Для примера привожу сравнение раскраски с jq
. В моей версии строки более яркие, а null имеет красный цвет для пущей заметности.
Понятно, что раскрашивание будет выполняться только при работе с терминалом.
Подключение 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
. Также буду рад услышать ваши пожелания по доработке и конструктивную критику.