├── .github └── workflows │ ├── build-exe.yml │ └── run-tests.yml ├── .gitignore ├── LICENSE ├── Makesurefile ├── README.md ├── article-1.png ├── article-ru.md ├── colorJson.js ├── jsqry-cli.js ├── jsqry.js ├── makesure ├── package-lock.json ├── package.json ├── prepare-for-qjs.py └── tests.tush /.github/workflows/build-exe.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: "Build executable" 5 | 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: macos-10.15 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - uses: actions/cache@v2 14 | id: cache-qjsc 15 | with: 16 | path: soft/quickjs-2020-11-08 17 | key: ${{ runner.os }}-quickjs-3 18 | 19 | - uses: actions/cache@v2 20 | id: cache-tush 21 | with: 22 | path: soft/tush 23 | key: ${{ runner.os }}-tush-2 24 | 25 | - name: "build & run tests" 26 | run: | 27 | ./makesure 28 | 29 | - uses: actions/upload-artifact@v2 30 | with: 31 | name: jsqry-macos 32 | path: build/jsqry 33 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | on: [ "push", "pull_request" ] 2 | 3 | name: "Build and test" 4 | 5 | jobs: 6 | build: 7 | name: "Run tests *nix" 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | # os: [ ubuntu-16.04, ubuntu-18.04, ubuntu-20.04, macos-10.15, macos-11.0 ] 12 | os: [ ubuntu-16.04, ubuntu-18.04, ubuntu-20.04, macos-10.15 ] 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions/cache@v2 17 | id: cache-qjsc 18 | with: 19 | path: soft/quickjs-2020-11-08 20 | key: ${{ runner.os }}-quickjs-3 21 | 22 | - uses: actions/cache@v2 23 | id: cache-tush 24 | with: 25 | path: soft/tush 26 | key: ${{ runner.os }}-tush-2 27 | 28 | - name: "build & run tests" 29 | run: | 30 | ./makesure tested 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | .idea/ 4 | target/ 5 | build/ 6 | soft/ 7 | temp/ 8 | node_modules/ 9 | *.iml 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jsqry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makesurefile: -------------------------------------------------------------------------------- 1 | 2 | @options timing 3 | 4 | @define QJS_VERSION="2021-03-27" 5 | @define QJS_HOME="soft/quickjs-$QJS_VERSION" 6 | 7 | 8 | @goal prepared_for_build 9 | @depends_on quickjs_installed build_folder_created 10 | 11 | @goal built_for_prod 12 | @depends_on prepared_for_build 13 | echo "Compiling release build with $($QJS_HOME/qjsc | grep version)..." 14 | 15 | cd build 16 | 17 | # -fno-json \ 18 | # -fno-module-loader \ 19 | # -fno-regexp \ 20 | # -fno-eval \ 21 | ../$QJS_HOME/qjsc \ 22 | -flto \ 23 | -fno-date \ 24 | -fno-proxy \ 25 | -fno-promise \ 26 | -fno-map \ 27 | -fno-typedarray \ 28 | -fno-string-normalize \ 29 | -fno-bigint \ 30 | -o jsqry ../jsqry-cli.js 31 | 32 | ls -lh jsqry 33 | 34 | @goal built_for_test 35 | @depends_on prepared_for_build 36 | echo "Compiling test build with $($QJS_HOME/qjsc | grep version)..." 37 | 38 | cd build 39 | 40 | ../$QJS_HOME/qjsc -o jsqry ../jsqry-cli.js 41 | 42 | ls -lh jsqry 43 | 44 | 45 | @goal build_folder_created 46 | @reached_if [[ -d "build" ]] 47 | mkdir build 48 | 49 | @goal soft_folder_created 50 | @reached_if [[ -d "soft" ]] 51 | mkdir soft 52 | 53 | @goal tush_installed 54 | @depends_on soft_folder_created 55 | @reached_if [[ -f "soft/tush/bin/tush-check" ]] 56 | echo 57 | echo "Fetching tush..." 58 | echo 59 | 60 | cd "soft" 61 | 62 | wget https://github.com/adolfopa/tush/archive/master.zip -O./tush.zip 63 | unzip ./tush.zip 64 | rm ./tush.zip 65 | mv tush-master tush 66 | 67 | @goal quickjs_installed 68 | @depends_on soft_folder_created 69 | @reached_if [[ -f "soft/quickjs-$QJS_VERSION/qjsc" ]] 70 | echo 71 | echo "Fetching QJS..." 72 | echo 73 | 74 | cd "soft" 75 | 76 | QJS=quickjs-$QJS_VERSION 77 | wget https://bellard.org/quickjs/$QJS.tar.xz 78 | tar xvf ./$QJS.tar.xz 79 | rm ./$QJS.tar.xz 80 | 81 | echo 82 | echo "Compile QJSC..." 83 | echo 84 | 85 | cd "$QJS" 86 | 87 | make qjsc libquickjs.a libquickjs.lto.a 88 | 89 | @goal soft_installed 90 | @depends_on tush_installed quickjs_installed 91 | 92 | @goal cleaned 93 | @reached_if [[ ! -f "build/jsqry" ]] 94 | rm "build/jsqry" 95 | 96 | @goal cleaned_soft 97 | @reached_if [[ ! -d "soft" ]] 98 | rm -r "soft" 99 | 100 | @goal cleaned_all 101 | @depends_on cleaned cleaned_soft 102 | 103 | @goal built 104 | @depends_on built_for_prod tests_executed 105 | 106 | @goal tested 107 | @depends_on built_for_test tests_executed 108 | 109 | @goal tests_executed 110 | @depends_on tush_installed 111 | export PATH="$MYDIR/build:$PATH:$MYDIR/soft/tush/bin" 112 | 113 | tush-check tests.tush && echo 'TESTS PASSED' || ( 114 | echo '!!! TESTS FAILED !!!' 115 | exit 1 116 | ) 117 | 118 | @goal default 119 | @depends_on built 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsqry-cli2 2 | 3 | [![Build and test](https://github.com/jsqry/jsqry-cli2/workflows/Build%20and%20test/badge.svg)](https://github.com/jsqry/jsqry-cli2/actions?query=workflow%3A%22Build+and+test%22) 4 | 5 | `jsqry` is a small command line tool (similar to [jq](https://github.com/stedolan/jq)) to query JSON using sane DSL. 6 | 7 | The purpose of this app is to expose the functionality of [jsqry](https://github.com/jsqry/jsqry) JS library in form of CLI. 8 | 9 | Unlike [jsqry-cli](https://github.com/jsqry/jsqry-cli) this one is based on [QuickJS](https://bellard.org/quickjs/) by [Fabrice Bellard](https://bellard.org/). 10 | 11 | ## Examples 12 | 13 | #### query 14 | ``` 15 | $ echo '[{"name":"John","age":30}, 16 | {"name":"Alice","age":25}, 17 | {"name":"Bob","age":50}]' | jsqry 'name' 18 | [ 19 | "John", 20 | "Alice", 21 | "Bob" 22 | ] 23 | ``` 24 | 25 | #### first element 26 | 27 | ``` 28 | $ echo '[{"name":"John","age":30}, 29 | {"name":"Alice","age":25}, 30 | {"name":"Bob","age":50}]' | jsqry -1 'name' 31 | "John" 32 | ``` 33 | 34 | #### use query parametrization 35 | 36 | ``` 37 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' \ 38 | | jsqry '[ _.age>=? && _.name.toLowerCase().startsWith(?) ]' --arg 30 --arg-str joh 39 | [ 40 | { 41 | "name": "John", 42 | "age": 30 43 | } 44 | ] 45 | ``` 46 | 47 | #### use as simple JSON pretty-printer 48 | 49 | ``` 50 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' | jsqry 51 | [ 52 | { 53 | "name": "John", 54 | "age": 30 55 | }, 56 | { 57 | "name": "Alice", 58 | "age": 25 59 | }, 60 | { 61 | "name": "Bob", 62 | "age": 50 63 | } 64 | ] 65 | ``` 66 | 67 | The output is pretty-printed by default. And colored! 68 | 69 | #### something trickier 70 | 71 | Filter greater than 2, map adding 100, sort descending, take last 2 elements. 72 | By combining these features you can build arbitrary complex queries. [Find more on supported DSL](https://jsqry.github.io/). 73 | 74 | ``` 75 | $ echo '[1,2,3,4,5]' | jsqry '[_>2] {_+100} s(-_) [-2:]' 76 | [ 77 | 104, 78 | 103 79 | ] 80 | ``` 81 | 82 | #### full JS power 83 | 84 | Since `jsqry` bundles the full-fledged [JS engine](https://bellard.org/quickjs/) in under 1 MB executable, the full power of JS is in your hands! 85 | 86 | ``` 87 | $ echo '["HTTP://EXAMPLE.COM/123", 88 | "https://www.Google.com/search?q=test", 89 | "https://www.YouTube.com/watch?v=_OBlgSz8sSM"]' | jsqry '{ _.match(/:\/\/([^\/]+)\//)[1].toLowerCase() }' 90 | [ 91 | "example.com", 92 | "www.google.com", 93 | "www.youtube.com" 94 | ] 95 | ``` 96 | 97 | #### help message 98 | 99 | ``` 100 | $ jsqry 101 | jsqry ver. 0.1.2 102 | Usage: echo $JSON | jsqry 'query' 103 | -1,--first return first result element 104 | -h,--help print help and exit 105 | -v,--version print version and exit 106 | -c,--compact compact output (no pretty-print) 107 | -u,--unquote unquote output string(s) 108 | -as ARG, 109 | --arg-str ARG supply string query argument 110 | -a ARG, 111 | --arg ARG supply query argument of any other type 112 | ``` 113 | 114 | ## Compare to jq 115 | 116 | https://gist.github.com/xonixx/d6066e83ec0773df248141440b18e8e4 117 | 118 | ## Install 119 | 120 | Current version: [0.1.2](https://github.com/jsqry/jsqry-cli2/releases/tag/v0.1.2). 121 | 122 | Sorry, but only Linux x64 is supported at the moment. Hopefully this will improve. 123 | 124 | To install or update the tool simply run the command below. 125 | 126 | ```bash 127 | $ sudo bash -e -c " 128 | wget https://github.com/jsqry/jsqry-cli2/releases/download/v0.1.2/jsqry-linux-amd64 -O/usr/local/bin/jsqry 129 | chmod +x /usr/local/bin/jsqry 130 | echo \"jsqry \$(jsqry -v) installed successfully\" 131 | " 132 | ``` -------------------------------------------------------------------------------- /article-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsqry/jsqry-cli2/7b0283eba50209559e7a6bf6e1c6532bee3c00c8/article-1.png -------------------------------------------------------------------------------- /article-ru.md: -------------------------------------------------------------------------------- 1 | # jsqry - лучше, чем jq 2 | 3 | В своей [прошлой статье на Хабре](https://habr.com/ru/post/303624/) я писал про библиотеку [Jsqry](https://github.com/jsqry/jsqry), которая предоставляет простой и удобный язык запросов (DSL) к объектам JSON. С тех пор прошло много времени и библиотека тоже получила свое развитие. Отдельный повод для гордости - библиотека имеет 98% покрытие кода тестами. Однако в этой статье речь не совсем о ней. 4 | 5 | Думаю, многие из вас знакомы с инструментом [`jq`](https://stedolan.github.io/jq/), который является практически стандартом де-факто для работы с JSON в командной строке и скриптах. Я тоже являлся её активным пользователем. Но меня все время беспокоила неоправданная сложность и неинтуитивность синтаксиса запросов этой утилиты. И не меня одного, вот лишь несколько цитат с [hacker news](https://news.ycombinator.com/): 6 | 7 | > I have been using jq for years and still can't get it to work quite how I would expect it to. 8 | 9 | > 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. 10 | 11 | > 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. 12 | 13 | > 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. 14 | 15 | > Let's just say it: jq is an amazing tool, but the DSL is just bad. 16 | 17 | > 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. 18 | 19 | Одним словом, вы уже наверное догадались. Пришла идея, а почему бы не обратить мою JS библиотеку в исполняемый файл для командной строки. Здесь есть один нюанс. Библиотека написана на JS и [её DSL](https://github.com/jsqry/jsqry.github.io/blob/master/README.md) также опирается на JS. Это значит, что надо найти способ упаковать программу и какой-нибудь JS-runtime в самодостаточный исполняемый файл. 20 | 21 | ## jsqry - GraalVM edition 22 | 23 | Для тех кто еще не в теме (неужели еще есть такие? oO) напомню, что [GraalVM](https://www.graalvm.org/) это такая прокачанная JVM от Oracle с дополнительными возможностями, самые заметные из которых: 24 | 25 | 1. Полиглотная JVM — возможность бесшовного совместного запуска Java, Javascript, Python, Ruby, R, и т.д. кода 26 | 2. Поддержка AOT-компиляции — компиляция Java прямо в нативный бинарник 27 | 3. Улучшения в JIT-компиляторе Java. 28 | 29 | Освежить представление о Graal можно, например, в [этой хабра-статье](https://habr.com/ru/company/haulmont/blog/433432/). 30 | 31 | Теоретически, объединение возможностей пунктов 1. и 2. должно решить поставленную задачу - обратить код на JS в исполняемый файл. 32 | 33 | Так родился проект https://github.com/jsqry/jsqry-cli. Правда, не спешите добавлять в закладки - в данный момент проект deprecated. Идея оказалась рабочей, но непрактичной. Дело в том, что размер исполняемого файла получался 99 Мб. Как-то не очень хорошо для простой утилиты командной строки. Тем более, если сравнить с `jq` с её размером 3.7 Мб для [последней версии для Linux 64](https://github.com/stedolan/jq/releases/tag/jq-1.6). 34 | 35 | В идеале хотелось бы получить размер не больше мегабайта. 36 | 37 | Тем не менее, решил оставить этот репозиторий как практический пример того как собрать из Java + JS кода исполняемый файл при помощи GraalVM. 38 | 39 | ## Небольшой обзор этого решения 40 | 41 | Решение имеет основной код запускаемого приложения в единственном файле [App.java](https://github.com/jsqry/jsqry-cli/blob/master/src/main/java/com/github/jsqry/cli/App.java). Этот код выполняет обработку параметров командной строки, используя стандартную java-библиотеку [Apache Commons CLI](https://commons.apache.org/proper/commons-cli/). 42 | 43 | Далее java-код вызывает код на javascript из файлов, находящихся в директории ресурсов src/main/resources. 44 | 45 | При этом интересный момент. Вроде-бы простейший код для вычитывания содержимого файла из ресурса 46 | 47 | ```java 48 | scripts.add(new String(Files.readAllBytes(Paths.get(jsFileResource.toURI())))); 49 | ``` 50 | 51 | под Граалем (то есть, будучи скомпилированным через [native-image](https://www.graalvm.org/reference-manual/native-image/)) падало с 52 | 53 | ``` 54 | java.nio.file.FileSystemNotFoundException: Provider "resource" not installed 55 | ``` 56 | 57 | Выручил древний "хак" для чтения строки из `InputStream` 58 | 59 | ```java 60 | scripts.add(new Scanner(jsFileResource.openStream()).useDelimiter("\\A").next()); 61 | ``` 62 | 63 | Короче говоря, надеяться на 100% поддержку всех функций стандартной Java Граалем все еще не приходится. 64 | 65 | Недавно аналогичной неприятной находкой оказалось [отсутствие поддержки java.awt.Graphics](https://github.com/oracle/graal/issues/1163). Это помешало использовать GraalVM для реализации AWS Lambda для конвертации картинок. 66 | 67 | 68 | ## jsqry - QuickJS edition 69 | 70 | Где-то в это же время я узнал о новом компактном движке JS [QuickJS](https://bellard.org/quickjs/) от гениального французского программиста [Фабриса Беллара](https://ru.wikipedia.org/wiki/%D0%91%D0%B5%D0%BB%D0%BB%D0%B0%D1%80,_%D0%A4%D0%B0%D0%B1%D1%80%D0%B8%D1%81). В своем составе этот инструмент несёт компилятор `qjsc` джаваскрипта в исполняемый файл. Также поддерживается почти полная совместимость с ES2020. То что нужно! 71 | 72 | Таким образом, появилась вторая инкарнация CLI-версии `jsqry`: https://github.com/jsqry/jsqry-cli2. 73 | Этот подход оказался более жизнеспособным и уже принес несколько релизов. 74 | 75 | Итак, что же такое `jsqry`? 76 | 77 | `jsqry` это маленькая утилита командной строки (похожая на [jq](https://stedolan.github.io/jq/)) для выполнения запросов к JSON используя "человеческий" DSL. 78 | 79 | Цель этой разработки - представить функционал JS библиотеки [jsqry](https://github.com/jsqry/jsqry) в форме интерфейса командной строки. 80 | 81 | ## Примеры использования 82 | 83 | #### запрос 84 | ``` 85 | $ echo '[{"name":"John","age":30}, 86 | {"name":"Alice","age":25}, 87 | {"name":"Bob","age":50}]' | jsqry 'name' 88 | [ 89 | "John", 90 | "Alice", 91 | "Bob" 92 | ] 93 | ``` 94 | 95 | #### первый элемент 96 | 97 | ``` 98 | $ echo '[{"name":"John","age":30}, 99 | {"name":"Alice","age":25}, 100 | {"name":"Bob","age":50}]' | jsqry -1 'name' 101 | "John" 102 | ``` 103 | 104 | #### использование параметризации запроса 105 | 106 | ``` 107 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' \ 108 | | jsqry '[ _.age>=? && _.name.toLowerCase().startsWith(?) ]' --arg 30 --arg-str joh 109 | [ 110 | { 111 | "name": "John", 112 | "age": 30 113 | } 114 | ] 115 | ``` 116 | 117 | #### использование в роли простого JSON pretty-printer 118 | 119 | ``` 120 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' \ 121 | | jsqry 122 | [ 123 | { 124 | "name": "John", 125 | "age": 30 126 | }, 127 | { 128 | "name": "Alice", 129 | "age": 25 130 | }, 131 | { 132 | "name": "Bob", 133 | "age": 50 134 | } 135 | ] 136 | ``` 137 | 138 | Выходной JSON утилиты по умолчанию отформатирован. И раскрашен! 139 | 140 | #### что-то более хитрое 141 | 142 | Отфильтровать элементы больше 2, добавить к каждому 100, отсортировать по убыванию и взять 2 последних элемента. Комбинируя эти возможности вы можете строить сколь угодно сложные запросы. [Узнать больше о поддерживаемом DSL](https://jsqry.github.io/). 143 | 144 | ``` 145 | $ echo '[1,2,3,4,5]' | jsqry '[_>2] {_+100} s(-_) [-2:]' 146 | [ 147 | 104, 148 | 103 149 | ] 150 | ``` 151 | 152 | #### полная мощь JS 153 | 154 | Поскольку `jsqry` вмещает полноценный [JS-движок](https://bellard.org/quickjs/) в исполняемом файле менее 1 Мб, полная мощь JS в ваших руках! 155 | 156 | ``` 157 | $ echo '["HTTP://EXAMPLE.COM/123", 158 | "https://www.Google.com/search?q=test", 159 | "https://www.YouTube.com/watch?v=_OBlgSz8sSM"]' \ 160 | | jsqry '{ _.match(/:\/\/([^\/]+)\//)[1].toLowerCase() }' 161 | [ 162 | "example.com", 163 | "www.google.com", 164 | "www.youtube.com" 165 | ] 166 | ``` 167 | 168 | #### help-сообщение 169 | 170 | ``` 171 | $ jsqry 172 | jsqry ver. 0.1.2 173 | Usage: echo $JSON | jsqry 'query' 174 | -1,--first return first result element 175 | -h,--help print help and exit 176 | -v,--version print version and exit 177 | -c,--compact compact output (no pretty-print) 178 | -u,--unquote unquote output string(s) 179 | -as ARG, 180 | --arg-str ARG supply string query argument 181 | -a ARG, 182 | --arg ARG supply query argument of any other type 183 | ``` 184 | 185 | ## Небольшое сравнение с `jq` 186 | 187 | А [здесь](https://gist.github.com/xonixx/d6066e83ec0773df248141440b18e8e4) я подготовил небольшое практическое сравнение `jq` и `jsqry` на примерах. 188 | 189 | ## Установка 190 | 191 | Текущая версия (на момент написания): [0.1.2](https://github.com/jsqry/jsqry-cli2/releases/tag/v0.1.2). 192 | 193 | К сожалению, только Linux x64 поддерживается в данный момент. Надеюсь, поддержка других платформ будет скоро добавлена. Буду рад здесь вашей помощи. 194 | 195 | Чтобы установить или обновить утилиту, просто выполните в командной строке приведенную ниже команду: 196 | 197 | ```bash 198 | $ sudo bash -e -c " 199 | wget https://github.com/jsqry/jsqry-cli2/releases/download/v0.1.2/jsqry-linux-amd64 -O/usr/local/bin/jsqry 200 | chmod +x /usr/local/bin/jsqry 201 | echo \"jsqry \$(jsqry -v) installed successfully\" 202 | " 203 | ``` 204 | 205 | ## О тестировании CLI-утилиты 206 | 207 | При разработке утилиты на GitHub хотелось реализовать какое-то подобие автоматического тестирования. Юнит-тесты довольно просто писать, когда вы работаете на уровне языка программирования. Интереснее дело обстоит, если хочется протестировать CLI-утилиту как единое целое, как черный ящик. Благо, в нашем случае это должно быть просто и логично, поскольку утилита представляет собой то, что функциональщики бы назвали [чистой функцией](https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8) - выход определяется исключительно входом. 208 | 209 | Попытав Гугл запросами вида "bash unit testing" и отметя варианты [BATS](https://opensource.com/article/19/2/testing-bash-bats), [ShellSpec](https://shellspec.info/), [Bach](https://bach.sh/) и несколько других подходов, как чересчур тяжеловесные для моего случая, а также самописную систему тестирования ([картинка про 14 стандартов](https://xkcd.ru/927/)), остановился на решении [tush](https://github.com/adolfopa/tush), гениальном в своей простоте. 210 | 211 | Тесты на `tush` представляют собой текстовый файл в таком синтаксисе 212 | 213 | ``` 214 | $ command --that --should --execute correctly 215 | | expected stdout output 216 | 217 | $ command --that --will --cause error 218 | @ expected stderr output 219 | ? expected-exit-code 220 | ``` 221 | 222 | Причем `tush` разбирает только строки начинающиеся на `$`, `|`, `@` и `?` - все остальные могут быть любым текстом, например описанием соответствующих тестов. При запуске теста инструмент запускает все строки, начинающиеся на `$` и просто сравнивает реальный вывод с ожидаемым, используя обычный `diff`. В случае отличия тест заканчивается неудачей, а diff отличия выводится пользователю. Пример: 223 | 224 | ``` 225 | $ /bin/bash /home/xonix/proj/jsqry-cli2/tests.sh 226 | --- tests.tush expected 227 | +++ tests.tush actual 228 | @@ -1,5 +1,5 @@ 229 | $ jsqry -v 230 | -| 0.1.2 231 | +| 0.1.1 232 | 233 | $ jsqry -h 234 | | jsqry ver. 0.1.1 235 | !!! TESTS FAILED !!! 236 | ``` 237 | 238 | Таким образом удалось покрыть тестами базовые сценарии работы с утилитой в виде одного файла 239 | [tests.tush](https://github.com/jsqry/jsqry-cli2/blob/master/tests.tush). 240 | 241 | Что особенно ценно, подобное тестовое описание одновременно может служить хорошую документирующую роль, демонстрируя типичные примеры использования. 242 | 243 | Удалось этот тестовый сценарий реализовать [в виде GitHub Action](https://github.com/jsqry/jsqry-cli2/blob/master/.github/workflows/run-tests.yml), который запускается на каждый коммит, гарантируя корректность каждого изменения и предоставляя замечательный бейдж: 244 | 245 | [![Build and test](https://github.com/jsqry/jsqry-cli2/workflows/Build%20and%20test/badge.svg)](https://github.com/jsqry/jsqry-cli2/actions?query=workflow%3A%22Build+and+test%22) 246 | 247 | ## Другие особенности решения 248 | 249 | ### Раскрашивание JSON 250 | 251 | Добавить раскраску выходного JSON оказалось на удивление [просто](https://github.com/jsqry/jsqry-cli2/blob/master/colorJson.js). Решение основано на подходе из проекта [zvakanaka/color-json](https://github.com/zvakanaka/color-json) с немного оптимизированными цветами, которые были подобраны на основе прекраснейшего StackOverflow [комментария](https://stackoverflow.com/a/28938235/104522). Для примера привожу сравнение раскраски с `jq`. В моей версии строки более яркие, а null имеет красный цвет для пущей заметности. 252 | 253 | ![screenshot](https://github.com/jsqry/jsqry-cli2/raw/master/article-1.png) 254 | 255 | Понятно, что раскрашивание будет выполняться только при работе с терминалом. 256 | 257 | ### Подключение npm-версии библиотеки в QuickJS 258 | 259 | Опишу немного процесс по которому [npm-версия](https://www.npmjs.com/package/jsqry) библиотеки `jsqry` подтягивается и включается в результирующий исполняемый файл. Для этого присутствует стандартный [package.json](https://github.com/jsqry/jsqry-cli2/blob/master/package.json) с необходимой версией библиотеки. Библиотека вытягивается стандартным `npm i`. Затем используется небольшой скрипт [prepare-for-qjs.py](https://github.com/jsqry/jsqry-cli2/blob/master/prepare-for-qjs.py), роль которого состоит в замене экспортирования в стиле nodejs на экспортирование в стиле модулей ES, только последнее поддерживается движком QuickJS. Далее уже полученный файл импортируется в основной код утилиты [jsqry-cli.js](https://github.com/jsqry/jsqry-cli2/blob/master/jsqry-cli.js). 260 | 261 | ### Чтение входной строки в UTF-8 в QuickJS 262 | 263 | В случае QuickJS некоторой мороки стоит считывание строки из stdin. Дело в том, что [минималистичная стандартная библиотека](https://bellard.org/quickjs/quickjs.html#Standard-library), доступная в QuickJS, реализует только считывание байтов. Поэтому понадобился некоторый [ручной код](https://github.com/jsqry/jsqry-cli2/blob/master/jsqry-cli.js#L8), чтоб перегнать байтики UTF-8 в JS-строку. К счастью, его не пришлось изобретать, а удалось позаимствовать из другого проекта на QuickJS: [twardoch/svgop](https://github.com/twardoch/svgop). 264 | 265 | ### Сборка утилиты 266 | 267 | Сборка утилиты выполняется скриптом [build.sh](https://github.com/jsqry/jsqry-cli2/blob/master/build.sh). 268 | 269 | Перечислю несколько "фишек" этого скрипта, которые оказались весьма полезными. 270 | 271 | Первое - скрипт безусловно вызывает в самом конце скрипт тестов [tests.sh](https://github.com/jsqry/jsqry-cli2/blob/master/tests.sh). Это гарантирует, что каждая вновь собранная версия утилиты будет протестирована, а сборка развалится если тесты будут неудачны. 272 | 273 | Второе - скрипт [build.sh](https://github.com/jsqry/jsqry-cli2/blob/master/build.sh) автоматически скачивает и компилирует заданную версию QuickJS, а скрипт [tests.sh](https://github.com/jsqry/jsqry-cli2/blob/master/tests.sh) выполняет то же для инструмента тестирования `tush`. Очень удобно - можно мгновенно продолжить разработку проекта на другой машине без лишних телодвижений. 274 | 275 | Третий момент. В самом конце сборки выполняется команда `ls -lh jsqry` чтоб показать размер результирующего файла. Как я уже упомянул, я немного параноидален на этот счет, хочется чтоб CLI-утилита имела наименьший размер. Я рад, что это принесло полезный побочный результат - помогло устранить [регрессию](https://www.freelists.org/post/quickjs-devel/Increased-compiled-binary-size-with-latest-quickjs-release-20200705), выявленную этой проверкой в одном из прошлых релизов QuickJS. 276 | 277 | В данный момент размер исполняемого файла составляет 652 KB. Мне кажется, довольно неплохо, учитывая что файл включает в себя полноценный движок современного стандарта JS. 278 | 279 | ## В качестве послесловия 280 | 281 | Прошу заинтересовавшихся попробовать представленную утилиту как замену `jq`. Также буду рад услышать ваши пожелания по доработке и конструктивную критику. -------------------------------------------------------------------------------- /colorJson.js: -------------------------------------------------------------------------------- 1 | // based on https://github.com/zvakanaka/color-json 2 | 3 | // https://stackoverflow.com/a/28938235/104522 4 | const colors = { 5 | separator: "\x1b[1m", 6 | string: "\x1b[0;92m", 7 | number: "\x1b[0m", 8 | boolean: "\x1b[1m", 9 | null: "\x1b[1;31m", 10 | key: "\x1b[34;1m", 11 | }; 12 | const RESET = "\x1b[0m"; 13 | 14 | export default function (jsonObj, spacing = 2) { 15 | const json = JSON.stringify(jsonObj, undefined, spacing); 16 | return ( 17 | colors.separator + 18 | json.replace( 19 | /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, 20 | function (match) { 21 | let colorCode = "number"; 22 | if (/^"/.test(match)) { 23 | if (/:$/.test(match)) { 24 | colorCode = "key"; 25 | match = match.substr(0, match.length - 1); 26 | } else colorCode = "string"; 27 | } else if (/true|false/.test(match)) colorCode = "boolean"; 28 | else if (/null/.test(match)) colorCode = "null"; 29 | const color = colors[colorCode] || ""; 30 | return `${color}${match}${RESET}${colors.separator}${ 31 | colorCode === "key" ? ":" : "" 32 | }`; 33 | } 34 | ) + 35 | RESET 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /jsqry-cli.js: -------------------------------------------------------------------------------- 1 | import * as std from "std"; 2 | import * as os from "os"; 3 | import jsqry from "./jsqry.js"; 4 | import colorJson from "./colorJson.js"; 5 | 6 | const VERSION = "0.1.2"; 7 | 8 | // based on code from https://github.com/twardoch/svgop/blob/master/src/app/svgop-qjs.js 9 | const utf8ArrayToStr = (function () { 10 | const charCache = new Array(128); // Preallocate the cache for the common single byte chars 11 | const charFromCodePt = String.fromCodePoint || String.fromCharCode; 12 | const result = []; 13 | 14 | return function (array) { 15 | let codePt, byte1; 16 | const buffLen = array.length; 17 | 18 | result.length = 0; 19 | 20 | for (let i = 0; i < buffLen; ) { 21 | byte1 = array[i++]; 22 | 23 | if (byte1 <= 0x7f) { 24 | codePt = byte1; 25 | } else if (byte1 <= 0xdf) { 26 | codePt = ((byte1 & 0x1f) << 6) | (array[i++] & 0x3f); 27 | } else if (byte1 <= 0xef) { 28 | codePt = 29 | ((byte1 & 0x0f) << 12) | 30 | ((array[i++] & 0x3f) << 6) | 31 | (array[i++] & 0x3f); 32 | } else if (String.fromCodePoint) { 33 | codePt = 34 | ((byte1 & 0x07) << 18) | 35 | ((array[i++] & 0x3f) << 12) | 36 | ((array[i++] & 0x3f) << 6) | 37 | (array[i++] & 0x3f); 38 | } else { 39 | codePt = 63; // Cannot convert four byte code points, so use "?" instead 40 | i += 3; 41 | } 42 | 43 | result.push( 44 | charCache[codePt] || (charCache[codePt] = charFromCodePt(codePt)) 45 | ); 46 | } 47 | 48 | return result.join(""); 49 | }; 50 | })(); 51 | 52 | function getstdin() { 53 | const result = []; 54 | let length = 0; 55 | let chunk; 56 | while (!std.in.eof()) { 57 | chunk = std.in.getByte(); 58 | if (chunk > 0) { 59 | result.push(chunk); 60 | length += chunk.length; 61 | } 62 | } 63 | return utf8ArrayToStr(result); 64 | } 65 | 66 | function err(msg) { 67 | std.err.puts(msg); 68 | std.err.puts("\n"); 69 | } 70 | 71 | const isTtyIn = os.isatty(0); 72 | const isTtyOut = os.isatty(1); 73 | 74 | function unquoteResult(res) { 75 | function isPrimitive(val) { 76 | const type = typeof val; 77 | return type === "string" || type === "boolean" || type === "number"; 78 | } 79 | if (isPrimitive(res)) { 80 | return [false, res]; 81 | } else if (Array.isArray(res)) { 82 | return [ 83 | res.length > 0, 84 | res.map((e) => (isPrimitive(e) ? e : JSON.stringify(e))).join("\n"), 85 | ]; 86 | } else { 87 | return [false, JSON.stringify(res)]; 88 | } 89 | } 90 | 91 | function doWork(jsonStr, queryStr, queryArgs, useFirst, compact, unquote) { 92 | let json; 93 | try { 94 | json = JSON.parse(jsonStr); 95 | } catch (e) { 96 | return "error: Wrong JSON"; 97 | } 98 | let res; 99 | try { 100 | res = (useFirst ? jsqry.first : jsqry.queryWithSingleMarker)( 101 | json, 102 | queryStr, 103 | ...queryArgs 104 | ); 105 | if (res != null && res._$single && res.length === 1) { 106 | res = res[0]; 107 | } 108 | } catch (e) { 109 | return "error: " + e; 110 | } 111 | if (unquote) { 112 | const [arrayNotEmpty, ur] = unquoteResult(res); 113 | if (ur === "") { 114 | if (arrayNotEmpty) { 115 | print(); 116 | } 117 | } else { 118 | print(ur); 119 | } 120 | } else { 121 | print( 122 | compact 123 | ? JSON.stringify(res) 124 | : isTtyOut 125 | ? colorJson(res, 2) 126 | : JSON.stringify(res, null, 2) 127 | ); 128 | } 129 | return null; 130 | } 131 | 132 | const QUERY_ARG_STR = "-as"; 133 | const QUERY_ARG_STR1 = "--arg-str"; 134 | const QUERY_ARG_OTHER = "-a"; 135 | const QUERY_ARG_OTHER1 = "--arg"; 136 | 137 | const valueSwitches = { 138 | [QUERY_ARG_STR]: 1, 139 | [QUERY_ARG_STR1]: 1, 140 | [QUERY_ARG_OTHER]: 1, 141 | [QUERY_ARG_OTHER1]: 1, 142 | }; 143 | 144 | const validSwitches = { 145 | "-1": 1, 146 | "--first": 1, 147 | "-h": 1, 148 | "--help": 1, 149 | "-v": 1, 150 | "--version": 1, 151 | "-c": 1, 152 | "--compact": 1, 153 | "-u": 1, 154 | "--unquote": 1, 155 | "-as": 1, 156 | "--arg-str": 1, 157 | "-a": 1, 158 | "--arg": 1, 159 | ...valueSwitches, 160 | }; 161 | 162 | function parseArgs() { 163 | const params = {}; 164 | const args = []; 165 | const queryArgs = []; 166 | let prevArg = null; 167 | 168 | for (let i = 1; i < scriptArgs.length; i++) { 169 | const arg = scriptArgs[i]; 170 | if (valueSwitches[prevArg]) { 171 | queryArgs.push([prevArg, arg]); 172 | prevArg = null; 173 | } else { 174 | if (arg.indexOf("-") === 0) { 175 | if (!valueSwitches[arg]) { 176 | params[arg] = true; 177 | } 178 | } else { 179 | args.push(arg); 180 | } 181 | prevArg = arg; 182 | } 183 | } 184 | 185 | return [params, args, queryArgs]; 186 | } 187 | 188 | const [params, args, queryArgs] = parseArgs(); 189 | 190 | const queryArgsParsed = queryArgs.map(([switch_, arg]) => 191 | QUERY_ARG_STR === switch_ || QUERY_ARG_STR1 === switch_ 192 | ? arg 193 | : JSON.parse(arg) 194 | ); 195 | 196 | const invalidSwitches = Object.keys(params).filter( 197 | (p) => !(p in validSwitches) 198 | ); 199 | if (invalidSwitches.length) { 200 | err("Invalid switches: " + invalidSwitches.join(",")); 201 | std.exit(1); 202 | } 203 | 204 | if (params["-v"] || params["--version"]) { 205 | print(VERSION); 206 | } else if ( 207 | params["-h"] || 208 | params["--help"] || 209 | (isTtyIn && scriptArgs.length === 1) /* called with no params */ 210 | ) { 211 | print(`jsqry ver. ${VERSION} 212 | Usage: echo $JSON | jsqry 'query' 213 | -1,--first return first result element 214 | -h,--help print help and exit 215 | -v,--version print version and exit 216 | -c,--compact compact output (no pretty-print) 217 | -u,--unquote unquote output string(s) 218 | -as ARG, 219 | --arg-str ARG supply string query argument 220 | -a ARG, 221 | --arg ARG supply query argument of any other type`); 222 | } else { 223 | const inputStr = getstdin(); 224 | const errMsg = doWork( 225 | inputStr, 226 | args[0] || "", 227 | queryArgsParsed, 228 | params["-1"] || params["--first"], 229 | params["-c"] || params["--compact"], 230 | params["-u"] || params["--unquote"] 231 | ); 232 | if (errMsg) { 233 | err(errMsg); 234 | std.exit(1); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /jsqry.js: -------------------------------------------------------------------------------- 1 | export default (function (root, factory) { return factory() 2 | })(this, function (undefined) { 3 | const fn = {}; 4 | const jsqry = { 5 | first, 6 | query, 7 | queryWithSingleMarker, 8 | cache: true, 9 | ast_cache: {}, 10 | fn, 11 | parse, 12 | printAst, 13 | }; 14 | 15 | const TYPE_PATH = 1; 16 | const TYPE_CALL = 2; 17 | const TYPE_FILTER = 3; 18 | const TYPE_NESTED_FILTER = 4; 19 | const TYPE_MAP = 5; 20 | const TYPE2STR = { 21 | [TYPE_PATH]: "p", 22 | [TYPE_FILTER]: "f", 23 | [TYPE_NESTED_FILTER]: "F", 24 | [TYPE_MAP]: "m", 25 | [TYPE_CALL]: "c", 26 | }; 27 | 28 | const SUB_TYPE_FUNC = 1; 29 | const SUB_TYPE_INDEX = 2; 30 | 31 | function printAst(ast) { 32 | const res = []; 33 | for (let i = 0; i < ast.length; i++) { 34 | const e = ast[i]; 35 | const t = e.type; 36 | let v = e.val; 37 | if (t === TYPE_CALL) { 38 | v = e.call + "," + v; 39 | } 40 | res.push(TYPE2STR[t] + "(" + v + ")"); 41 | } 42 | return res.join(" "); 43 | } 44 | 45 | function isArr(obj) { 46 | if (obj == null) return false; 47 | return obj.length !== undefined && typeof obj !== "string"; 48 | } 49 | 50 | function funcToken(token) { 51 | token.sub_type = SUB_TYPE_FUNC; 52 | token.func = Function("_,i,args,f,q", "return " + token.val); 53 | } 54 | 55 | const goodPathRe = /^[A-Za-z0-9_]*$/; 56 | 57 | function parse(expr, arg_idx0) { 58 | let cached; 59 | if (jsqry.cache && (cached = jsqry.ast_cache[expr])) { 60 | return cached; 61 | } 62 | 63 | const expr0 = expr; 64 | arg_idx0 = arg_idx0 || 0; 65 | let arg_idx = arg_idx0; 66 | const ast = []; 67 | let token = { type: TYPE_PATH, val: "" }; 68 | let depth_filter = 0; // nesting of [] 69 | let depth_nested_filter = 0; // nesting of <<>> 70 | let depth_map = 0; // nesting of {} 71 | let depth_call = 0; // nesting of () 72 | let prevType = null; 73 | let currStrQuote = null; 74 | let i; // pos 75 | 76 | function startNewTok(type) { 77 | let val = (token.val = token.val.trim()); 78 | // console.info( 79 | // `startNewTok ${TYPE2STR[type]} i=${i} "${expr.substr(i)}" val="${val}"` 80 | // ); 81 | const prevPrevType = prevType; 82 | prevType = token.type; 83 | if (token.call) { 84 | token.call = token.call.trim(); 85 | } 86 | if ( 87 | type === null && 88 | (prevType === TYPE_FILTER || 89 | prevType === TYPE_MAP || 90 | prevType === TYPE_CALL) 91 | ) { 92 | throw ( 93 | "Not closed " + 94 | (prevType === TYPE_FILTER 95 | ? "[" 96 | : prevType === TYPE_MAP 97 | ? "{" 98 | : prevType === TYPE_CALL 99 | ? "(" 100 | : "wtf") 101 | ); 102 | } 103 | if (!val && prevType === TYPE_CALL) { 104 | // handle 's()' 105 | val = token.val = "_"; 106 | } 107 | if ( 108 | prevType === TYPE_PATH && 109 | ((prevPrevType === TYPE_PATH && !val) || 110 | (val !== "*" && !goodPathRe.test(val))) 111 | ) { 112 | throw 'Illegal path element "' + val + '" at pos ' + i; 113 | } 114 | if (val) { 115 | // handle prev token 116 | ast.push(token); 117 | if (prevType === TYPE_FILTER) { 118 | if (val.indexOf("_") >= 0 || val.indexOf("i") >= 0) { 119 | // function 120 | funcToken(token); 121 | } else { 122 | // index/slice 123 | token.sub_type = SUB_TYPE_INDEX; 124 | const idx = val.split(":"); 125 | token.index = idx; 126 | for (let j = 0; j < idx.length; j++) { 127 | const v = idx[j].trim(); 128 | const vI = parseInt(v); 129 | if (v && isNaN(vI)) { 130 | throw 'Not an int slice index: "' + v + '"'; 131 | } 132 | idx[j] = vI; 133 | } 134 | } 135 | } else if (prevType === TYPE_NESTED_FILTER) { 136 | const _ast = jsqry.parse(val, arg_idx); 137 | arg_idx += _ast.args_count; 138 | token.func = function (e, i, args) { 139 | const res = _queryAst(e, _ast, args); 140 | for (let j = 0; j < res.length; j++) { 141 | if (res[j]) { 142 | return true; 143 | } 144 | } 145 | return false; 146 | }; 147 | } else if ( 148 | prevType === TYPE_MAP || 149 | (prevType === TYPE_CALL && token.call) 150 | ) { 151 | funcToken(token); 152 | } 153 | } 154 | token = { type: type, val: "" }; 155 | } 156 | 157 | for (i = 0; i < expr.length; i++) { 158 | const l = expr[i], 159 | next = expr[i + 1], 160 | prev = expr[i - 1]; 161 | if (l === ".") { 162 | if (token.type === TYPE_PATH) { 163 | startNewTok(TYPE_PATH); 164 | } else { 165 | token.val += l; 166 | } 167 | } else if (l === "?" && token.type !== TYPE_PATH) { 168 | if (currStrQuote === null) { 169 | if (next === "?") { 170 | token.val += l; 171 | i++; 172 | } else { 173 | token.val += "args[" + arg_idx++ + "]"; 174 | } 175 | } else { 176 | token.val += l; 177 | } 178 | } else if (l === "[") { 179 | if (depth_filter === 0 && token.type === TYPE_PATH) { 180 | startNewTok(TYPE_FILTER); 181 | } else { 182 | token.val += l; 183 | } 184 | if (token.type === TYPE_FILTER) { 185 | depth_filter++; 186 | } 187 | } else if (l === "]") { 188 | if (token.type === TYPE_PATH) { 189 | throw "] without ["; 190 | } 191 | if (token.type === TYPE_FILTER && --depth_filter === 0) { 192 | if (!token.val.trim()) { 193 | throw "Empty []"; 194 | } 195 | startNewTok(TYPE_PATH); 196 | } else { 197 | token.val += l; 198 | } 199 | } else if (l === "<" && next === "<") { 200 | i++; 201 | if (depth_nested_filter === 0 && token.type === TYPE_PATH) { 202 | startNewTok(TYPE_NESTED_FILTER); 203 | } else { 204 | token.val += "<<"; 205 | } 206 | if (token.type === TYPE_NESTED_FILTER) { 207 | depth_nested_filter++; 208 | } 209 | } else if (l === ">" && next === ">") { 210 | i++; 211 | if (token.type === TYPE_PATH) { 212 | throw ">> without <<"; 213 | } 214 | if (token.type === TYPE_NESTED_FILTER && --depth_nested_filter === 0) { 215 | if (!token.val.trim()) { 216 | throw "Empty <<>>"; 217 | } 218 | startNewTok(TYPE_PATH); 219 | } else { 220 | token.val += ">>"; 221 | } 222 | } else if (l === "{") { 223 | if (depth_map === 0 && token.type === TYPE_PATH) { 224 | startNewTok(TYPE_MAP); 225 | } else { 226 | token.val += l; 227 | } 228 | if (token.type === TYPE_MAP) { 229 | depth_map++; 230 | } 231 | } else if (l === "}") { 232 | if (token.type === TYPE_PATH) { 233 | throw "} without {"; 234 | } 235 | if (token.type === TYPE_MAP && --depth_map === 0) { 236 | if (!token.val.trim()) { 237 | throw "Empty {}"; 238 | } 239 | startNewTok(TYPE_PATH); 240 | } else { 241 | token.val += l; 242 | } 243 | } else if (l === "(") { 244 | if (depth_call === 0 && token.type === TYPE_PATH) { 245 | token.call = token.val; 246 | token.val = ""; 247 | token.type = TYPE_CALL; 248 | } else { 249 | token.val += l; 250 | } 251 | if (token.type === TYPE_CALL) { 252 | depth_call++; 253 | } 254 | } else if (l === ")") { 255 | if (token.type === TYPE_PATH) { 256 | throw ") without ("; 257 | } 258 | if (token.type === TYPE_CALL && --depth_call === 0) { 259 | startNewTok(TYPE_PATH); 260 | } else { 261 | token.val += l; 262 | } 263 | } else if ( 264 | (l === '"' || l === "'" || l === "`") && 265 | token.type !== TYPE_PATH 266 | ) { 267 | if (currStrQuote === l) { 268 | if (prev !== "\\") { 269 | currStrQuote = null; 270 | } 271 | } else { 272 | currStrQuote = l; 273 | } 274 | 275 | token.val += l; 276 | } else { 277 | token.val += l; 278 | } 279 | } 280 | 281 | startNewTok(null); // close 282 | 283 | ast.args_count = arg_idx - arg_idx0; 284 | 285 | if (jsqry.cache) { 286 | jsqry.ast_cache[expr0] = ast; 287 | } 288 | return ast; 289 | } 290 | 291 | function first(obj, expr) { 292 | const res = query.apply(null, arguments); 293 | return res.length ? res[0] : null; 294 | } 295 | 296 | function query(obj, expr) { 297 | const res = queryWithSingleMarker.apply(null, arguments); 298 | delete res._$single; 299 | return res; 300 | } 301 | 302 | function queryWithSingleMarker(obj, expr) { 303 | const args = Array.prototype.slice.call(arguments, 2); 304 | const ast = jsqry.parse(expr); 305 | if (args.length !== ast.args_count) throw "Wrong args count"; 306 | return _queryAst(obj, ast, args); 307 | } 308 | 309 | function _queryAst(obj, ast, args) { 310 | if (!obj) return []; 311 | if (!isArr(obj)) { 312 | obj = [obj]; 313 | obj._$single = true; 314 | } 315 | 316 | for (let i = 0; i < ast.length; i++) { 317 | obj = exec(obj, ast[i], args); 318 | } 319 | 320 | return obj; 321 | } 322 | 323 | function normIdx(is_from, idx, len, step) { 324 | if (isNaN(idx)) 325 | idx = is_from ? (step > 0 ? 0 : -1) : step > 0 ? len : -len - 1; 326 | if (idx < 0) idx += len; 327 | return idx; 328 | } 329 | 330 | function calcIndex(list, index) { 331 | // console.info('idx', list, index) 332 | const res = []; 333 | if (list._$single) res._$single = true; 334 | const idx_cnt = index.length; 335 | const len = list.length; 336 | if (idx_cnt === 1) { 337 | const val = list[normIdx(1, index[0], len)]; 338 | if (val !== undefined) res.push(val); 339 | } else if (idx_cnt >= 2) { 340 | let step = idx_cnt === 3 ? index[2] : 1; 341 | if (isNaN(step)) step = 1; 342 | const from = normIdx(1, index[0], len, step); 343 | const to = normIdx(0, index[1], len, step); 344 | for (let i = from; step > 0 ? i < to : i > to; i += step) { 345 | const val = list[i]; 346 | if (val !== undefined) res.push(val); 347 | } 348 | } 349 | return res; 350 | } 351 | 352 | function sortFn(a, b) { 353 | return a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0; 354 | } 355 | 356 | fn.s = function (pairs, res) { 357 | pairs.sort(sortFn); 358 | for (let i = 0; i < pairs.length; i++) { 359 | res.push(pairs[i][0]); 360 | } 361 | }; 362 | fn.u = function (pairs, res) { 363 | const exists = {}; 364 | for (let i = 0; i < pairs.length; i++) { 365 | const p = pairs[i]; 366 | if (!exists[p[1]]) { 367 | exists[p[1]] = 1; 368 | res.push(p[0]); 369 | } 370 | } 371 | }; 372 | fn.g = function (pairs, res) { 373 | const groups = {}; 374 | for (let i = 0; i < pairs.length; i++) { 375 | const p = pairs[i]; 376 | let group = groups[p[1]]; 377 | if (!group) group = groups[p[1]] = [p[1], []]; 378 | group[1].push(p[0]); 379 | } 380 | for (let k in groups) { 381 | const g = groups[k]; 382 | res.push([g[0], g[1]]); 383 | } 384 | }; 385 | 386 | function exec(data, token, args) { 387 | // console.log('Exec', data, token); 388 | let res = []; 389 | if (data._$single) res._$single = true; 390 | 391 | function _applyFunc() { 392 | for (let i = 0; i < data.length; i++) { 393 | const v = data[i]; 394 | if (token.func(v, i, args, first, query)) { 395 | res.push(v); 396 | } 397 | } 398 | } 399 | 400 | if (token.type === TYPE_PATH) { 401 | for (let i = 0; i < data.length; i++) { 402 | let v = (data[i] || {})[token.val]; 403 | if (v === undefined && "*" === token.val) { 404 | v = data[i]; 405 | } 406 | if (isArr(v)) { 407 | delete res._$single; 408 | for (let j = 0; j < v.length; j++) { 409 | res.push(v[j]); 410 | } 411 | } else if (v !== undefined && v !== null) { 412 | res.push(v); 413 | } 414 | } 415 | } else if (token.type === TYPE_FILTER) { 416 | if (token.sub_type === SUB_TYPE_FUNC) { 417 | _applyFunc(); 418 | } else if (token.sub_type === SUB_TYPE_INDEX) { 419 | res = calcIndex(data, token.index); 420 | } 421 | } else if (token.type === TYPE_NESTED_FILTER) { 422 | _applyFunc(); 423 | } else if (token.type === TYPE_MAP) { 424 | for (let i = 0; i < data.length; i++) { 425 | res.push(token.func(data[i], i, args, first, query)); 426 | } 427 | } else if (token.type === TYPE_CALL) { 428 | const fname = token.call; 429 | const f = fn[fname]; 430 | if (!f) throw 'not valid call: "' + fname + '"'; 431 | const pairs = []; 432 | for (let i = 0; i < data.length; i++) { 433 | const v = data[i]; 434 | pairs.push([v, token.func(v, i, args, first, query)]); 435 | } 436 | f(pairs, res); 437 | } 438 | 439 | return res; 440 | } 441 | 442 | return jsqry; 443 | }); 444 | -------------------------------------------------------------------------------- /makesure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION="0.9.7.1" 4 | 5 | exec awk -v "Version=$VERSION" -v "Prog=$0" ' 6 | BEGIN { 7 | Shell = "bash" # default shell 8 | SupportedShells["bash"] 9 | SupportedShells["sh"] 10 | SupportedOptions["tracing"] 11 | SupportedOptions["silent"] 12 | SupportedOptions["timing"] 13 | Tmp = isDir("/dev/shm") ? "/dev/shm" : "/tmp" 14 | split("",Lines) 15 | split("",Args) # parsed CLI args 16 | split("",ArgGoals) # invoked goals 17 | split("",Options) 18 | split("",GoalNames) # list 19 | split("",GoalsByName) # name -> "" 20 | split("",Code) # name -> body 21 | split("",DefineOverrides) # k -> v 22 | DefinesFile="" 23 | split("",Dependencies) # name,i -> dep goal 24 | split("",DependenciesCnt) # name -> dep cnd 25 | split("",Doc) # name,i -> doc str 26 | split("",DocCnt) # name -> doc lines cnt 27 | split("",ReachedIf) # name -> condition line 28 | split("",GlobFiles) # list 29 | Mode = "prelude" # prelude/goal/goal_glob 30 | srand() 31 | prepareArgs() 32 | MyDirScript = "MYDIR=" quoteArg(getMyDir(ARGV[1])) "; export MYDIR; cd \"$MYDIR\"" 33 | } 34 | { Lines[NR]=$0 } 35 | "@options" == $1 { handleOptions(); next } 36 | "@define" == $1 { handleDefine(); next } 37 | "@shell" == $1 { handleShell(); next } 38 | "@goal" == $1 { handleGoal(); next } 39 | "@goal_glob" == $1 { handleGoalGlob(); next } 40 | "@doc" == $1 { handleDoc(); next } 41 | "@depends_on" == $1 { handleDependsOn(); next } 42 | "@reached_if" == $1 { handleReachedIf(); next } 43 | { handleCodeLine($0); next } 44 | 45 | END { if (!Died) doWork() } 46 | 47 | function prepareArgs( i,arg) { 48 | for (i = 2; i < ARGC; i++) { 49 | arg = ARGV[i] 50 | #print i " " arg 51 | if (substr(arg,1,1) == "-") { 52 | if (arg == "-f" || arg == "--file") { 53 | delete ARGV[i] 54 | ARGV[1] = ARGV[++i] 55 | } else if (arg == "-D" || arg == "--define") { 56 | delete ARGV[i] 57 | handleOptionDefineOverride(ARGV[++i]) 58 | } else 59 | Args[arg] 60 | } else 61 | arrPush(ArgGoals, arg) 62 | 63 | delete ARGV[i] # https://unix.stackexchange.com/a/460375 64 | } 65 | if ("-h" in Args || "--help" in Args) { 66 | print "makesure ver. " Version 67 | print "Usage: makesure [options...] [-f buildfile] [goals...]" 68 | print " -f,--file buildfile" 69 | print " set buildfile to use (default Makesurefile)" 70 | print " -l,--list list all available goals" 71 | print " -d,--resolved list resolved dependencies to reach given goals" 72 | print " -D \"var=val\",--define \"var=val\"" 73 | print " override @define values" 74 | print " -s,--silent silent mode - only output what goals output" 75 | print " -t,--timing display execution times for goals and total" 76 | print " -x,--tracing enable tracing in bash/sh via `set -x`" 77 | print " -v,--version print version and exit" 78 | print " -h,--help print help and exit" 79 | print " -U,--selfupdate update makesure to latest version" 80 | realExit(0) 81 | } else if ("-v" in Args || "--version" in Args) { 82 | print Version 83 | realExit(0) 84 | } else if ("-U" in Args || "--selfupdate" in Args) { 85 | selfUpdate() 86 | realExit(0) 87 | } 88 | if (!isFile(ARGV[1])) { 89 | if (isFile(ARGV[1] "/Makesurefile")) 90 | ARGV[1] = ARGV[1] "/Makesurefile" 91 | else 92 | dieMsg("makesure file not found: " ARGV[1]) 93 | } 94 | if ("-s" in Args || "--silent" in Args) 95 | Options["silent"] 96 | if ("-x" in Args || "--tracing" in Args) 97 | Options["tracing"] 98 | if ("-t" in Args || "--timing" in Args) 99 | Options["timing"] 100 | } 101 | 102 | function dbgA(name, arr, i) { print "--- " name ": "; for (i in arr) print i " : " arr[i] } 103 | 104 | function splitKV(arg, kv, n) { 105 | n = index(arg, "=") 106 | kv[0] = trim(substr(arg,1,n-1)) 107 | kv[1] = trim(substr(arg,n+1)) 108 | } 109 | function handleOptionDefineOverride(arg, kv) { 110 | handleDefineLine(arg) 111 | splitKV(arg, kv) 112 | DefineOverrides[kv[0]] = kv[1] 113 | } 114 | 115 | function handleOptions() { 116 | checkPreludeOnly() 117 | 118 | for (i=2; i<=NF; i++) { 119 | if (!($i in SupportedOptions)) 120 | die("Option " $i " is not supported") 121 | Options[$i] 122 | } 123 | } 124 | 125 | function handleDefine( line,kv) { 126 | $1 = "" 127 | handleDefineLine($0) 128 | } 129 | function handleDefineLine(line, kv) { 130 | checkPreludeOnly() 131 | 132 | if (!DefinesFile) 133 | DefinesFile = executeGetLine("mktemp " Tmp "/makesure.XXXXXXXXXX") 134 | 135 | splitKV(line, kv) 136 | 137 | if (!(kv[0] in DefineOverrides)) { 138 | handleCodeLine(line) 139 | handleCodeLine("echo " quoteArg(line) " >> " DefinesFile) 140 | } 141 | } 142 | 143 | function handleShell() { 144 | checkPreludeOnly() 145 | 146 | Shell = trim($2) 147 | 148 | if (!(Shell in SupportedShells)) 149 | die("Shell '\''" Shell "'\'' is not supported") 150 | } 151 | 152 | function adjustOptions() { 153 | if ("silent" in Options) 154 | delete Options["timing"] 155 | } 156 | 157 | function started(mode) { 158 | if (isPrelude()) adjustOptions() 159 | Mode = mode 160 | } 161 | 162 | function handleGoal() { 163 | started("goal") 164 | registerGoal($2) 165 | } 166 | 167 | function registerGoal(goal_name) { 168 | goal_name = trim(goal_name) 169 | if (length(goal_name) == 0) { 170 | die("Goal must have a name") 171 | } 172 | if (goal_name in GoalsByName) { 173 | die("Goal " goal_name " is already defined") 174 | } 175 | arrPush(GoalNames, goal_name) 176 | GoalsByName[goal_name] 177 | } 178 | 179 | function calcGlob(pattern, script, file) { 180 | split("",GlobFiles) 181 | script = MyDirScript "; for f in ./" pattern " ; do test -e \"$f\" && echo \"$f\" ; done" 182 | while (script | getline file) { 183 | file = substr(file, 3) 184 | arrPush(GlobFiles,file) 185 | } 186 | close(script) 187 | } 188 | 189 | function handleGoalGlob( goal_name,i) { 190 | started("goal_glob") 191 | $1 = "" 192 | calcGlob(trim($0)) 193 | for (i=0; i " dep 310 | topologicalSortAddConnection(goal_name, dep) 311 | } else { 312 | #print " [ reached] " goal_name " -> " dep 313 | } 314 | } 315 | 316 | goal_body[0] = "" 317 | if (!("silent" in Options)) { 318 | addStr(goal_body, "echo \" goal '\''" goal_name "'\'' ") 319 | if (reached_goals[goal_name]) 320 | addStr(goal_body, "[already satisfied].") 321 | else if (empty_goals[goal_name]) 322 | addStr(goal_body, "[empty].") 323 | else 324 | addStr(goal_body, "...") 325 | addStr(goal_body, "\"") 326 | } 327 | if (reached_goals[goal_name]) 328 | addLine(goal_body, "exit 0") 329 | 330 | addLine(goal_body, defines_line[0]) 331 | 332 | if ("tracing" in Options) 333 | addLine(goal_body, "set -x") 334 | 335 | addLine(goal_body, body) 336 | goal_bodies[goal_name] = goal_body[0] 337 | } 338 | 339 | resolveGoalsToRun(resolved_goals) 340 | 341 | if ("-d" in Args || "--resolved" in Args) { 342 | printf("Resolved goals to reach for '\''%s'\'':\n", join(ArgGoals, 0, arrLen(ArgGoals), " ")) 343 | for (i = 0; i < arrLen(resolved_goals); i++) { 344 | print " " resolved_goals[i] 345 | } 346 | } else { 347 | for (i = 0; i < arrLen(resolved_goals); i++) { 348 | goal_name = resolved_goals[i] 349 | goal_timed = "timing" in Options && !reached_goals[goal_name] && !empty_goals[goal_name] 350 | if (goal_timed) 351 | t1 = t2 ? t2 : currentTimeMillis() 352 | exit_code = shellExec(goal_bodies[goal_name]) 353 | if (exit_code != 0) 354 | print " goal '\''" goal_name "'\'' failed" 355 | if (goal_timed) { 356 | t2 = currentTimeMillis() 357 | print " goal '\''" goal_name "'\'' took " renderDuration(t2 - t1) 358 | } 359 | if (exit_code != 0) 360 | break 361 | } 362 | if ("timing" in Options) 363 | print " total time " renderDuration((t2 ? t2 : currentTimeMillis()) - t0) 364 | if (exit_code != 0) 365 | realExit(exit_code) 366 | } 367 | 368 | realExit(0) 369 | } 370 | } 371 | 372 | function resolveGoalsToRun(result, i, goal_name, loop) { 373 | if (arrLen(ArgGoals) == 0) 374 | arrPush(ArgGoals, "default") 375 | 376 | for (i = 0; i < arrLen(ArgGoals); i++) { 377 | goal_name = ArgGoals[i] 378 | if (!(goal_name in GoalsByName)) { 379 | dieMsg("Goal not found: " goal_name) # TODO can we show line number here? 380 | } 381 | topologicalSortPerform(goal_name, result, loop) 382 | } 383 | 384 | if (loop[0] == 1) { 385 | dieMsg("There is a loop in goal dependencies via " loop[1] " -> " loop[2]) 386 | } 387 | } 388 | 389 | function isPrelude() { return "prelude"==Mode } 390 | function checkPreludeOnly() { if (!isPrelude()) die("Only use " $1 " in prelude") } 391 | function checkGoalOnly() { if ("goal" != Mode && "goal_glob" != Mode) die("Only use " $1 " in goal/goal_glob") } 392 | function currentGoalName() { return isPrelude() ? "" : arrLast(GoalNames) } 393 | 394 | function realExit(code, i) { 395 | Died = 1 396 | if (DefinesFile) 397 | system("rm " DefinesFile) 398 | exit code 399 | } 400 | function die(msg, n) { if (!n) n=NR; dieMsg(msg ":\n" ARGV[1] ":" n ": " Lines[n]) } 401 | function dieMsg(msg, out) { 402 | out = "cat 1>&2" # trick to write from awk to stderr 403 | print msg | out 404 | close(out) 405 | realExit(1) 406 | } 407 | 408 | function checkConditionReached(defines_line, condition_str, script) { 409 | script = defines_line # need this to initialize variables for check conditions 410 | script = script "\n" condition_str 411 | #print "script: " script 412 | return shellExec(script) == 0 413 | } 414 | 415 | function shellExec(script, res) { 416 | script = Shell " -e -c " quoteArg(script) 417 | 418 | #print script 419 | res = system(script) 420 | #print "res " res 421 | return res 422 | } 423 | 424 | function getMyDir(makesurefilePath) { 425 | return executeGetLine("cd \"$(dirname " quoteArg(makesurefilePath) ")\"; pwd") 426 | } 427 | 428 | function handleCodeLine(line, goal_name) { 429 | if ("goal_glob" == Mode) { 430 | for (i=0; i " newVer 488 | } else print "you have latest version " Version " installed" 489 | } 490 | system("rm " tmp) 491 | if (err) dieMsg(err); 492 | } 493 | 494 | function renderDuration(deltaMillis,\ 495 | deltaSec,deltaMin,deltaHr,deltaDay,dayS,hrS,minS,secS,secSI,res) { 496 | 497 | deltaSec = deltaMillis / 1000 498 | deltaMin = 0 499 | deltaHr = 0 500 | deltaDay = 0 501 | 502 | if (deltaSec >= 60) { 503 | deltaMin = int(deltaSec / 60) 504 | deltaSec = deltaSec - deltaMin * 60 505 | } 506 | 507 | if (deltaMin >= 60) { 508 | deltaHr = int(deltaMin / 60) 509 | deltaMin = deltaMin - deltaHr * 60 510 | } 511 | 512 | if (deltaHr >= 24) { 513 | deltaDay = int(deltaHr / 24) 514 | deltaHr = deltaHr - deltaDay * 24 515 | } 516 | 517 | dayS = deltaDay > 0 ? deltaDay " d" : "" 518 | hrS = deltaHr > 0 ? deltaHr " h" : "" 519 | minS = deltaMin > 0 ? deltaMin " m" : "" 520 | secS = deltaSec > 0 ? deltaSec " s" : "" 521 | secSI = deltaSec > 0 ? int(deltaSec) " s" : "" 522 | 523 | if (dayS != "") 524 | res = dayS " " (hrS == "" ? "0 h" : hrS) 525 | else if (deltaHr > 0) 526 | res = hrS " " (minS == "" ? "0 m" : minS) 527 | else if (deltaMin > 0) 528 | res = minS " " (secSI == "" ? "0 s" : secSI) 529 | else 530 | res = deltaSec > 0 ? secS : "0 s" 531 | 532 | return res 533 | } 534 | function executeGetLine(script, res) { 535 | script | getline res 536 | close(script) 537 | return res 538 | } 539 | function dl(url, dest, verbose) { 540 | verbose = "VERBOSE" in ENVIRON 541 | if (commandExists("wget")) { 542 | if (!ok("wget " (verbose ? "" : "-q") " " quoteArg(url) " -O" quoteArg(dest))) 543 | return "error with wget" 544 | } else if (commandExists("curl")) { 545 | if (!ok("curl " (verbose ? "" : "-s") " " quoteArg(url) " -o " quoteArg(dest))) 546 | return "error with curl" 547 | } else return "wget/curl no found" 548 | } 549 | function join(arr, start_incl, end_excl, sep, result, i) { 550 | result = arr[start_incl] 551 | for (i = start_incl + 1; i < end_excl; i++) 552 | result = result sep arr[i] 553 | return result 554 | } 555 | function addStr(target, str) { target[0] = target[0] str } 556 | function addLine(target, line) { target[0] = addL(target[0], line) } 557 | function addL(s, l) { return s ? s "\n" l : l } 558 | function arrPush(arr, elt) { arr[arr[-7]++] = elt } 559 | function arrLen(arr) { return 0 + arr[-7] } 560 | function arrLast(arr) { return arr[arrLen(arr)-1] } 561 | function commandExists(cmd) { return ok("which " cmd " 2>&1 >/dev/null") } 562 | function ok(cmd) { return system(cmd) == 0 } 563 | function isFile(path) { return ok("test -f " quoteArg(path)) } 564 | function isDir(path) { return ok("test -d " quoteArg(path)) } 565 | function quoteArg(a) { gsub("'\''", "'\''\\'\'''\''", a); return "'\''" a "'\''" } 566 | function trim(s) { sub(/^[ \t\r\n]+/, "", s); sub(/[ \t\r\n]+$/, "", s); return s; } 567 | ' Makesurefile "$@" 568 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "jsqry": { 6 | "version": "1.2.4", 7 | "resolved": "https://registry.npmjs.org/jsqry/-/jsqry-1.2.4.tgz", 8 | "integrity": "sha512-1tKcFIGpHaUdyXlFS5BFNZ4E3iM6a98tFas7HgWY4ppHdgw7VcWg5AZ2GTVlBnadRdM4aRa6Cn3XXl3yh4AonQ==" 9 | }, 10 | "prettier": { 11 | "version": "2.1.2", 12 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz", 13 | "integrity": "sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg==", 14 | "dev": true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jsqry": "1.2.4" 4 | }, 5 | "devDependencies": { 6 | "prettier": "2.1.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /prepare-for-qjs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | s = open('node_modules/jsqry/jsqry.js', 'r', encoding='UTF-8').read() 4 | splitPart = '})(this, function (undefined) {' 5 | parts = s.split(splitPart) 6 | s = 'export default (function (root, factory) { return factory()\n' + splitPart + parts[1] 7 | open('jsqry.js', 'w', encoding='UTF-8').write(s) 8 | print('SUCCESS') 9 | -------------------------------------------------------------------------------- /tests.tush: -------------------------------------------------------------------------------- 1 | $ jsqry -v 2 | | 0.1.2 3 | 4 | $ jsqry -h 5 | | jsqry ver. 0.1.2 6 | | Usage: echo $JSON | jsqry 'query' 7 | | -1,--first return first result element 8 | | -h,--help print help and exit 9 | | -v,--version print version and exit 10 | | -c,--compact compact output (no pretty-print) 11 | | -u,--unquote unquote output string(s) 12 | | -as ARG, 13 | | --arg-str ARG supply string query argument 14 | | -a ARG, 15 | | --arg ARG supply query argument of any other type 16 | 17 | $ echo '[{"a":1},{"a":2}]' | jsqry 'a' 18 | | [ 19 | | 1, 20 | | 2 21 | | ] 22 | 23 | $ echo '[{"a":1},{"a":2}]' | jsqry # pretty-print 24 | | [ 25 | | { 26 | | "a": 1 27 | | }, 28 | | { 29 | | "a": 2 30 | | } 31 | | ] 32 | 33 | $ echo '[{"a":1},{"a":2}]' | jsqry -1 'a' # first 34 | | 1 35 | 36 | $ echo '["a", "b", "c", "d", "e"]' | jsqry '[i%2==0]' 37 | | [ 38 | | "a", 39 | | "c", 40 | | "e" 41 | | ] 42 | 43 | $ echo '[{ "k": [{ "a": 1 }, { "a": 2 }] }, { "k": [{ "a": 3 }] }]' | jsqry 'k.*.a' 44 | | [ 45 | | 1, 46 | | 2, 47 | | 3 48 | | ] 49 | 50 | $ echo '[{ "k": [{ "a": 1 }, { "a": 2 }] }, { "k": [{ "a": 3 }] }]' | jsqry 'k.*.a[::-1]' 51 | | [ 52 | | 3, 53 | | 2, 54 | | 1 55 | | ] 56 | 57 | $ echo '[{ "k": [{ "a": 1 }, { "a": 2 }] }, { "k": [{ "a": 3 }] }]' | jsqry -1 'k.*.a[::-1][_<3][1]{_+100}' 58 | | 101 59 | 60 | $ echo 'hello' | jsqry 'a' 61 | @ error: Wrong JSON 62 | ? 1 63 | 64 | $ echo '{}' | jsqry '[' 65 | @ error: Not closed [ 66 | ? 1 67 | 68 | $ echo '{}' | jsqry '}' 69 | @ error: } without { 70 | ? 1 71 | 72 | === Tests for the documentation examples === 73 | 74 | $ printf '[{"name":"John","age":30},\n\t{"name":"Alice","age":25},\n\t{"name":"Bob","age":50}]' | jsqry 'name' 75 | | [ 76 | | "John", 77 | | "Alice", 78 | | "Bob" 79 | | ] 80 | 81 | $ printf '[{"name":"John","age":30},\n\t{"name":"Alice","age":25},\n\t{"name":"Bob","age":50}]' | jsqry 'name' -c 82 | | ["John","Alice","Bob"] 83 | 84 | $ printf '[{"name":"John","age":30},\n\t{"name":"Alice","age":25},\n\t{"name":"Bob","age":50}]' | jsqry -1 'name' 85 | | "John" 86 | 87 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' | jsqry '[ _.age>=? && _.name.toLowerCase().startsWith(?) ]' --arg 30 --arg-str joh 88 | | [ 89 | | { 90 | | "name": "John", 91 | | "age": 30 92 | | } 93 | | ] 94 | 95 | $ echo '[{"name":"John","age":30},{"name":"Alice","age":25},{"name":"Bob","age":50}]' | jsqry 96 | | [ 97 | | { 98 | | "name": "John", 99 | | "age": 30 100 | | }, 101 | | { 102 | | "name": "Alice", 103 | | "age": 25 104 | | }, 105 | | { 106 | | "name": "Bob", 107 | | "age": 50 108 | | } 109 | | ] 110 | 111 | $ echo '[1,2,3,4,5]' | jsqry '[_>2] {_+100} s(-_) [-2:]' 112 | | [ 113 | | 104, 114 | | 103 115 | | ] 116 | 117 | $ printf '["HTTP://EXAMPLE.COM/123",\n\t"https://www.Google.com/search?q=test",\n\t"https://www.YouTube.com/watch?v=_OBlgSz8sSM"]' | jsqry '{ _.match(/:\/\/([^\/]+)\//)[1].toLowerCase() }' 118 | | [ 119 | | "example.com", 120 | | "www.google.com", 121 | | "www.youtube.com" 122 | | ] 123 | 124 | === Test arguments === 125 | 126 | $ echo '[{"name": "Bob", "age": 30}, {"name": "Alice", "age": 25}]' | jsqry '[_.name==?].age' --arg-str Alice 127 | | [ 128 | | 25 129 | | ] 130 | 131 | $ echo 1 | jsqry '{[?,?,?,?,?,?]}' -as str -a false --arg null -a 2e5 -a [1,2,3] -a '{"K":"V"}' 132 | | [ 133 | | "str", 134 | | false, 135 | | null, 136 | | 200000, 137 | | [ 138 | | 1, 139 | | 2, 140 | | 3 141 | | ], 142 | | { 143 | | "K": "V" 144 | | } 145 | | ] 146 | 147 | $ echo [2,1,3] | jsqry '{ ?[_] }' --arg '{"1":"one", "2":"two", "3": "three"}' 148 | | [ 149 | | "two", 150 | | "one", 151 | | "three" 152 | | ] 153 | 154 | $ echo 1 | jsqry '{[?,?,?]}' -as -a -as "qqq" --arg-str z 155 | | [ 156 | | "-a", 157 | | "qqq", 158 | | "z" 159 | | ] 160 | 161 | === unquoted output === 162 | 163 | $ echo '[{"name":"Bob"}, {"name": "Alice"}]' | jsqry 'name' --unquote 164 | | Bob 165 | | Alice 166 | 167 | $ echo '[{"name":"Bob"}, {"name": "Alice"}]' | jsqry 'name' -1 -u 168 | | Bob 169 | 170 | $ echo '[{"name":"Bob"}, {"name": "Alice"}, {"name": {"value":"John"}}]' | jsqry 'name' -u 171 | | Bob 172 | | Alice 173 | | {"value":"John"} 174 | 175 | $ echo '[]' | jsqry -u # no output 176 | 177 | $ echo '[]' | jsqry -u | wc -l | tr -d ' ' # https://apple.stackexchange.com/questions/370366/why-is-wc-c-printing-spaces-before-the-number 178 | | 0 179 | 180 | $ echo '[""]' | jsqry -u 181 | | 182 | | 183 | 184 | $ echo '[""]' | jsqry -u | wc -l | tr -d ' ' 185 | | 1 186 | 187 | $ echo '[null,"null","\"test quotes"]' | jsqry -u 188 | | null 189 | | null 190 | | "test quotes 191 | 192 | $ echo '{"json":"{ \"a\": \"b\" }"}' | jsqry 'json' -u | jsqry 193 | | { 194 | | "a": "b" 195 | | } 196 | 197 | === Embedded queries === 198 | 199 | $ echo '[{ "name": "Alice", "props": [{ "key": "age", "val": 30 }, { "key": "car", "val": "Volvo" }] },{ "name": "Bob", "props": [{ "key": "age", "val": 40 }] },{ "name": "John", "props": [] }]' | jsqry '{ _.name + " : " + (f(_.props,"[_.key===`age`].val")||"") }' 200 | | [ 201 | | "Alice : 30", 202 | | "Bob : 40", 203 | | "John : " 204 | | ] 205 | 206 | $ echo '[{ "name": "Alice", "props": [{ "key": "age", "val": 30 }, { "key": "car", "val": "Volvo" }] },{ "name": "Bob", "props": [{ "key": "age", "val": 40 }] },{ "name": "John", "props": [] }]' | jsqry '{ _.name + " : " + (f(_.props,"[_.key===?].val", "age")||"") }' 207 | | [ 208 | | "Alice : 30", 209 | | "Bob : 40", 210 | | "John : " 211 | | ] 212 | 213 | $ echo '[{ "name": "Alice", "props": [{ "key": "age", "val": 30 }, { "key": "car", "val": "Volvo" }] },{ "name": "Bob", "props": [{ "key": "age", "val": 40 }] },{ "name": "John", "props": [] }]' | jsqry '{ _.name + " : " + (f(_.props,"[_.key===?].val", ?)||"") }' -as "age" 214 | | [ 215 | | "Alice : 30", 216 | | "Bob : 40", 217 | | "John : " 218 | | ] 219 | 220 | === Invalid switches === 221 | 222 | $ echo 1 | jsqry -abc --unquote -1 --QWE 223 | @ Invalid switches: -abc,--QWE 224 | ? 1 225 | 226 | $ jsqry -abc --QWE 227 | @ Invalid switches: -abc,--QWE 228 | ? 1 229 | 230 | === Segfault #1 === 231 | 232 | $ echo '[{"a":1},{"a":"hello"}]' | jsqry '[_.a>1]' 233 | | [] 234 | 235 | $ echo '[{"a":1},{"a":"hello"}, {"a":2}]' | jsqry '[_.a>1]' 236 | | [ 237 | | { 238 | | "a": 2 239 | | } 240 | | ] 241 | 242 | === https://github.com/jsqry/jsqry-cli2/issues/24 === 243 | 244 | $ echo '{"a":123}' | jsqry 245 | | { 246 | | "a": 123 247 | | } 248 | 249 | --------------------------------------------------------------------------------