jq — самая популярная утилита для обработки JSON из командной строки, написана на C и имеет свой собственный синтаксис для работы с JSON.
Однако, обрабатывать JSON в командной строке не нужно очень часто, а когда потребность возникает, приходится мучиться с незнакомым языком программирования.
Так и появилась идея написать fx с простым и понятным синтаксисом, который никогда не забудешь. А какой язык программирования знают все? Правильно — JavaScript.
fx принимает в качестве аргумента код на JavaScript, содержащий анонимную функцию, и вызывает её, подставляя в качестве аргумента JSON, полученный из stdin. То, что функция вернёт, будет выведено в stdout. Все. Больше никаких сложных и непонятных конструкций.
Сравните два решения одной и той же задачи на jq и на fx:
jq '[.order[] as $x | .data[$x]]'
fx 'input => input.order.map(x => obj.data[x])'
Чуть более многословно? Да, это же просто обычный JavaScript.
Если fx вообще не передать аргументов, то JSON будет отформатирован и выведен:
$ echo '{"key":"value"}' | fx
{
"key": "value"
}
Если код не содержит param =>
, то переданный код будет автоматически преобразован в анонимную функцию и this
будет содержать переданный JSON объект:
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
"value"
fx можно передать несколько аргументов/анонимных функций, они будут применены поочерёдно к JSON:
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
"value"
Если код содержит ключевое слово yield
, созданная анонимная функция будет содержать "развернутый" генератор (пример из generator-expression):
$ cat data.json | fx '\
for (let user of this) \
if (user.login.startsWith("a")) \
yield user'
$ echo '["a", "b"]' | fx 'yield* this'
[
"a",
"b"
]
Это позволяет описывать очень простые выборки в сложных запросах.
Кстати, модифицировать JSON с fx тоже очень просто:
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
А также можно использовать любой npm пакет, если поставить его глобально:
$ npm install -g lodash
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
Для некоторых важно, что jq всего лишь один бинарник (~2mb). Так вот fx тоже имеет отдельные бинарники.
Весят они немного больше (~30mb), но если вам это не критично, и стоит nodejs, то поставить fx можно с помощью npm:
npm install -g fx
А что по производительности? Давайте замерим с помощью hyperfine:
$ curl 'https://api.github.com/repos/stedolan/jq/commits' > data.json
$ hyperfine --warmup 3 "jq ..." "fx ..."
Benchmark #1: cat data.json | jq '[.[] | {message: .commit.message, name: .commit.committer.name}]'
Time (mean ± σ): 10.7 ms ± 0.9 ms [User: 8.7 ms, System: 3.6 ms]
Range (min … max): 9.0 ms … 14.9 ms
Benchmark #2: cat data.json | fx 'this.map(({commit}) => ({message: commit.message, name: commit.committer.name}))'
Time (mean ± σ): 159.6 ms ± 4.4 ms [User: 127.4 ms, System: 28.4 ms]
Range (min … max): 153.0 ms … 170.0 ms
На порядок меньше. А все дело в том, что fx использует eval
для запуска кода из аргументов (и вообще весь код fx <70 строк кода). Если для вас важна скорость обработки, не используйте fx. Во всех остальных случаях — fx отличный выбор.
Надеюсь, эта утилита кому-нибудь пригодится. Спасибо за внимание.