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

    В своей прошлой статье на Хабре я писал про библиотеку 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. Также буду рад услышать ваши пожелания по доработке и конструктивную критику.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 15

      –3

      Нода как runtime-среда для сервера? Спасибо, не надо. Т.е. я ценю попытку, и будь это более системный язык (C/C++/Rust/Go/perl/python/awk/bash), оно бы прокатило. Делать node системной ради одного query — нет, спасибо, не надо.

        +7
        Возможно, я не вполне понятно описал, но ноды как рантайм-среды там нет ни в каком виде. Утилита представляет собой самодостаточный исполняемый файл, который имеет составной частью движок QuickJS для реализации логики DSL.
        +3
        … компилятор qjsc джаваскрипта в исполняемый файл

        Это не совсем компилятор, т.е. производительность всё равно будет в десятки раз меньше чем V8 после jit, для больших JSON (или много мелких) запросов не особо-то разгуляешься.


        А в остальном тулза хорошая получилась, гораздо приятней (и понятней) чем jq.

          +2

          Что стараетесь держать размер исполняемого файла небольшим это круто, моё почтение! Но как по мне потребление RAM и CPU важнее. Можете проделать какие-нибудь бенчмарки в сравнении с jshon и jq?

            +2

            Было бы хорошо, если бы вы привели примеры использования jq и вашей утилиты на подобных вещах: "Отфильтровать элементы больше 2, добавить к каждому 100, отсортировать по убыванию и взять 2 последних элемента."


            Чтобы предметно увидеть преимущества и простоту вашего DSL по сравнению с jq.


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

              +1
              Конкретно этот пример будет так (похоже, но на jq длиннее)

              $ echo '[1,2,3,4,5]' | jq 'map(select(.>2)) | map(.+100) | sort_by(-.) | .[-2:]'
              [
              104,
              103
              ]

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

              Тут больше сравнительных примеров: gist.github.com/xonixx/d6066e83ec0773df248141440b18e8e4
              +6

              На мой взгляд у jq наркоманский синтаксис, но у вас получилось сделать ещё более наркоманский и менее понятный синтаксис

                +3

                На самом деле синтаксис простой, если понять строительные блоки, коих немного. Это:


                1. field1.field2.field3 — доступ по полям
                2. [ expression ] — фильтрация
                3. { expression } — трансформация (map)
                4. [ from:to:step ] — срезы в стиле Python
                5. s( expression ) — сортировка

                Где expression — выражение с "_" которое под капотом соответствует JS функции function(_) { return expression }. Например, фильтр [ _ > 5 ] соответствует предикату function(_) { return _ > 5 }. Все, этого знания достаточно чтоб написать 90% типовых запросов.

                +1
                Я собрал программу на 32-хразрядной винде, подсветка не работает, но тесты проходят.
                  +1
                  а сделайте пожалуйста pull request?
                  а на 64bit win не получается собрать?
                    +1
                    Я пока не занимался правкой скриптов. Задача была проверить возможность сборки, поэтому я сделал это вручную. Для сборки использовался MinGW, поэтому, думаю, без проблем можно собрать и 64-битную версию. Главная проблема, собрать QuickJS, покуда в его Makefil'е присутствуют изъяны: например, строка «HOST_LIBS=-lm -ldl -lpthread» на Windows является лишней.
                  +1
                  а какова все таки производительность?
                    0

                    Интересно, а jsonnet не рассматривали в качестве альтернативы?

                      +1
                      Кстати, есть еще pwsh:
                      Write-Host '[1,2,3,4,5]' | ConvertFrom-Json -Depth 99 | Where-Object {$_ -gt 2} | ForEach-Object{$_+100} | Select-Object -Last 2

                      Или короче:
                      echo '[1,2,3,4,5]' | ConvertFrom-Json -Depth 99 | ?{$_ -gt 2} | %{$_+100} | select -Last 2


                      Замер скорости работы этого примера на случайном linux-сервере:
                      jq(time, real):~ 7мс
                      jsqry(time, real):~ 5мс
                      pwsh(Measure-Command):~ 4мс
                      (НО! Первый старт pwsh был ~50мс, последующие быстрее, даже если менять предикаты или кол-во входных элементов)
                        0
                        *Write-Output

                        Only users with full accounts can post comments. Log in, please.