├── .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 | [](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 | [](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 | 
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 |
--------------------------------------------------------------------------------