8 个稳定版本
| 1.5.0 | 2024 年 6 月 26 日 |
|---|---|
| 1.4.0 | 2024 年 5 月 23 日 |
| 1.2.1 | 2024 年 1 月 22 日 |
| 1.2.0 | 2023 年 11 月 17 日 |
| 1.0.0-beta | 2023 年 8 月 4 日 |
在 编程语言 中排名第 8
每月下载量 25,096
被 12 个 包(9 个直接)使用
225KB
4K SLoC
jaq
jaq(发音类似于 Jacques[^jacques])是 JSON 数据处理工具 jq 的克隆。jaq 旨在支持 jq 的语法和操作的很大一部分。
您可以在 jaq playground 上在线尝试 jaq。playground 的说明可以在这里找到:这里。
jaq 专注于三个目标
-
正确性:jaq 旨在提供比 jq 更正确和可预测的实现,同时在大多数情况下保持与 jq 的兼容性。
一些令人惊讶的 jq 行为示例
nan > nan为假,而nan < nan为真。[[]] | implode导致 jq 崩溃,尽管 这个问题已经知道五年了,但直到写作时仍未得到修复。- 《jq 手册》声称,
limit(n; exp)"从exp中提取最多n个输出"。这适用于n > 1的情况,例如jq -n '[limit(2; 1, 2, 3)]'生成[1, 2],但当n == 0时,jq -n '[limit(0; 1, 2, 3)]'生成[1]而不是[]。更糟糕的是,当n < 0时,limit会生成所有来自exp的输出,而这在文档中没有说明。
-
性能:我最初创建jaq是因为我对jq的长时间启动时间感到烦恼,在我的机器上大约为50ms。这在大量处理小型文件时尤为明显。jaq的启动速度比jq 1.6快30倍,并且在许多其他基准测试中也优于jq。
-
简洁性:jaq旨在具有简单、小巧的实现,以减少潜在的错误并便于贡献。
我从另一个Rust程序中汲取了灵感,即jql。然而,与jql不同,jaq旨在尽可能接近jq的语法和语义。这应该使用户能够轻松使用jaq。
[^jacques]:我想创建一个像优秀服务员一样低调而乐于助人的工具。当我想到一个典型的法国服务员的名字时,我脑海中浮现出“Jacques”。后来,我了解到古老的法语单词jacquet,意为“松鼠”,这为名字提供了一个很好的事后灵感。
安装
二进制文件
您可以在发行页面上下载Linux、Mac和Windows的二进制文件。
您还可以使用homebrew在macOS或Linux上安装jaq
$ brew install jaq
$ brew install --HEAD jaq # latest development version
或在Windows上使用scoop
$ scoop install main/jaq
从源代码
要编译jaq,您需要一个Rust工具链。有关说明,请参阅https://rustup.rs/。(请注意,Linux发行版中提供的Rust编译器可能过于过时,无法编译jaq。)
以下任一命令都可以安装jaq
$ cargo install --locked jaq
$ cargo install --locked --git https://github.com/01mf02/jaq # latest development version
在我的系统中,这两个命令都将可执行文件放置在~/.cargo/bin/jaq。
如果您已克隆此存储库,您还可以通过执行克隆存储库中的其中一个命令来构建jaq
$ cargo build --release # places binary into target/release/jaq
$ cargo install --locked --path jaq # installs binary
jaq应该在Rust支持的任何系统上工作。如果不起作用,请提交问题。
示例
以下示例可以给您一个jaq当前能做什么的印象。您应该通过将jaq替换为jq来获得相同的输出。如果不是这样,请提交问题将受到赞赏。 :) 语法在jq手册中有文档。
访问字段
$ echo '{"a": 1, "b": 2}' | jaq '.a'
1
添加值
$ echo '{"a": 1, "b": 2}' | jaq 'add'
3
以两种方式从对象构建数组,并显示它们相等
$ echo '{"a": 1, "b": 2}' | jaq '[.a, .b] == [.[]]'
true
对数组的所有元素应用过滤器并过滤结果
$ echo '[0, 1, 2, 3]' | jaq 'map(.*2) | [.[] | select(. < 5)]'
[0, 2, 4]
将输入值读入数组并计算其元素的平均值
$ echo '1 2 3 4' | jaq -s 'add / length'
2.5
重复应用过滤器并输出中间结果
$ echo '0' | jaq '[recurse(.+1; . < 3)]'
[0, 1, 2]
懒折叠输入并输出中间结果
$ seq 1000 | jaq -n 'foreach inputs as $x (0; . + $x)'
1 3 6 10 15 [...]
性能
以下评估包含几个基准测试,允许比较jaq、jq和gojq的性能。空(empty)基准测试运行n次带有null输入的过滤器empty,用于测量启动时间。bf-fib基准测试运行一个用jq编写的Brainfuck解释器,解释一个生成n斐波那契数的Brainfuck脚本。其他基准测试使用n作为输入评估各种过滤器;有关详细信息,请参阅bench.sh。
我在一个装有AMD Ryzen 5 5500U的Linux系统上使用bench.sh target/release/jaq jq-1.7 gojq-0.1213 jq-1.6 | tee bench.json生成了基准测试数据。然后,我用一个“单行”(稍微拉伸了这个术语和行)处理了结果
jq -rs '.[] | "|`\(.name)`|\(.n)|" + ([.time[] | min | (.*1000|round)? // "N/A"] | min as $total_min | map(if . == $total_min then "**\(.)**" else "\(.)" end) | join("|"))' bench.json
(当然,您也可以在这里用jaq代替jq。)最后,我将表头与输出连接起来,并通过pandoc -t gfm管道传输。
[^binaries]:jq-1.7.1和gojq-0.12.15的二进制文件是从它们的GitHub发布页面检索的,jq-1.6的二进制文件是从标准的Ubuntu仓库安装的。
表:以毫秒为单位的评估结果(如果超过10秒则为“N/A”)。
| 基准测试 | n | jaq-1.4 | jq-1.7.1 | gojq-0.12.15 | jq-1.6 |
|---|---|---|---|---|---|
empty |
512 | 610 | 660 | 740 | 8310 |
bf-fib |
13 | 470 | 1220 | 570 | 1440 |
reverse |
1048576 | 50 | 680 | 270 | 650 |
sort |
1048576 | 140 | 550 | 580 | 680 |
group-by |
1048576 | 400 | 1890 | 1550 | 2860 |
min-max |
1048576 | 210 | 320 | 250 | 350 |
add |
1048576 | 520 | 640 | 1310 | 730 |
kv |
131072 | 170 | 140 | 220 | 190 |
kv-update |
131072 | 190 | 540 | 440 | N/A |
kv-entries |
131072 | 630 | 1150 | 830 | 1120 |
ex-implode |
1048576 | 510 | 1100 | 610 | 1090 |
reduce |
1048576 | 820 | 890 | N/A | 860 |
try-catch |
1048576 | 180 | 320 | 370 | 670 |
tree-flatten |
17 | 730 | 360 | 10 | 480 |
tree-update |
17 | 560 | 970 | 1330 | 1190 |
tree-paths |
17 | 470 | 250 | 880 | 460 |
to-fromjson |
65536 | 30 | 370 | 120 | 390 |
ack |
7 | 530 | 700 | 1230 | 620 |
range-prop |
128 | 280 | 310 | 210 | 590 |
结果表明,jaq-1.4在15个基准测试中是最快的,而jq-1.7.1在2个基准测试中是最快的,gojq-0.12.15在2个基准测试中是最快的。因为gojq实现了过滤器flatten的原生实现而不是定义实现,所以它在tree-flatten上要快得多。
特性
以下是一个概述,总结了
- 已实现的功能,以及
- 尚未实现的功能。
基础
- 身份(
.) - 递归(
..) - 基本数据类型(null,布尔值,数字,字符串,数组,对象)
- if-then-else(
if .a < .b then .a else .b end) - 折叠(
reduce .[] as $x (0; . + $x),foreach .[] as $x (0; . + $x; . + .)) - 错误处理(
try ... catch ...)(参见与jq的差异) - 字符串插值(
"The successor of \(.) is \(.+1).") - 格式化字符串(
@json,@text,@csv,@tsv,@html,@sh,@base64,@base64d)
路径
- 数组/对象的索引(
.[0],.a,.["a"]) - 遍历数组/对象(
.[]) - 可选索引/迭代(
.a?,.[]?) - 数组切片(
.[3:7],.[0:-1]) - 字符串切片
运算符
- 组合(
|) - 绑定(
. as $x | $x) - 连接(
,) - 直接赋值(
=) - 更新赋值(
|=,+=,-=) - 替代(
//) - 逻辑(
or,and) - 相等和比较(
.a == .b,.a < .b) - 算术(
+,-,*,/,%) - 否定(
-) - 错误抑制(
?)
定义
- 基本定义 (
def map(f): [.[] | f];) - 递归定义 (
def r: r; r)
核心过滤器
- 空 (
empty) - 错误 (
error) - 输入 (
inputs) - 长度 (
length,utf8bytelength) - 舍入 (
floor,round,ceil) - 字符串与JSON互转 (
fromjson,tojson) - 字符串与整数互转 (
explode,implode) - 字符串规范化 (
ascii_downcase,ascii_upcase) - 字符串前缀/后缀 (
startswith,endswith,ltrimstr,rtrimstr) - 字符串分割 (
split("foo")) - 数组过滤器 (
reverse,sort,sort_by(-.),group_by,min_by,max_by) - 流消费者 (
first,last,range,fold) - 流生成器 (
range,recurse) - 时间 (
now,fromdateiso8601,todateiso8601) - 更多数值过滤器 (
sqrt,sin,log,pow, ...) (数值过滤器列表) - 更多时间过滤器 (
strptime,strftime,strflocaltime,mktime,gmtime, 和localtime)
标准过滤器
这些过滤器通过更基本的过滤器定义。它们的定义在 std.jq。
- 未定义 (
null) - 布尔值 (
true,false,not) - 特殊数字 (
nan,infinite,isnan,isinfinite,isfinite,isnormal) - 类型 (
type) - 过滤 (
select(. >= 0)) - 选择 (
values,nulls,booleans,numbers,strings,arrays,objects,iterables,scalars) - 转换 (
tostring,tonumber) - 可迭代过滤器(
map(.+1),map_values(.+1),add,join("a")) - 数组过滤器(
transpose,first,last,nth(10),flatten,min,max) - 对象-数组转换(
to_entries,from_entries,with_entries) - 通用/存在性(
all,any) - 递归(
walk) - I/O(
input) - 正则表达式(
test,scan,match,capture,splits,sub,gsub) - 时间(
fromdate,todate)
数字过滤器
jaq 从 libm 导入了许多过滤器,并遵循它们的类型签名。
jaq 中定义的数字过滤器的完整列表
零参数过滤器
-
acos -
acosh -
asin -
asinh -
atan -
atanh -
cbrt -
cos -
cosh -
erf -
erfc -
exp -
exp10 -
exp2 -
expm1 -
fabs -
frexp,它返回一个 (float, integer) 对。 -
ilogb,它返回整数。 -
j0 -
j1 -
lgamma -
log -
log10 -
log1p -
log2 -
logb -
modf,它返回一个 (float, float) 对。 -
nearbyint -
pow10 -
rint -
significand -
sin -
sinh -
sqrt -
tan -
tanh -
tgamma -
trunc -
y0 -
y1
忽略 . 的双参数过滤器
-
atan2 -
copysign -
drem -
fdim -
fmax -
fmin -
fmod -
hypot -
jn,它将整数作为第一个参数。 -
ldexp,它将整数作为第二个参数。 -
nextafter -
nexttoward -
pow -
remainder -
scalb -
scalbln,它将整数作为第二个参数。 -
yn,它将整数作为第一个参数。
忽略 . 的三参数过滤器
-
fma
高级功能
jaq 目前并不旨在支持 jq 的几个功能,例如
- 模块
- SQL 风格的运算符
- 流式处理
jq 和 jaq 之间的差异
数字
jq 使用 64 位浮点数(floats)表示任何数字。相比之下,jaq 将数字 0 或 -42 解释为机器大小的整数,将数字 0.0 或 3e8 解释为 64 位浮点数。jaq 中的许多操作,如数组索引,都会检查传递的数字是否确实是整数。背后的动机是避免可能无声地导致错误结果的舍入误差。例如
$ jq -n '[0, 1, 2] | .[1.0000000000000001]'
1
$ jaq -n '[0, 1, 2] | .[1.0000000000000001]'
Error: cannot use 1.0 as integer
$ jaq -n '[0, 1, 2] | .[1]'
1
jaq 的规则是
- 两个整数的和、差、积和余数是整数。
- 两个数字之间的任何其他操作都产生浮点数。
示例
$ jaq -n '1 + 2'
3
$ jaq -n '10 / 2'
5.0
$ jaq -n '1.0 + 2'
3.0
您可以通过添加0.0、乘以1.0或除以1将整数转换为浮点数,例如。您可以通过round、floor或ceil将浮点数转换为整数。
$ jaq -n '1.2 | [floor, round, ceil]'
[1, 1, 2]
NaN和无穷大
在jq中,除以0有一些令人惊讶的特性;例如,0 / 0会产生nan,而0 as $n | $n / 0会产生一个错误。在jaq中,n / 0如果n == 0则产生nan,如果n > 0则产生infinite,如果n < 0则产生-infinite。jaq的行为更接近浮点数算术的IEEE标准(IEEE 754)。
jaq在浮点数上实现了一个全序,以便对值进行排序。因此,它不幸地必须强制执行nan == nan。 (jq通过强制执行nan < nan来解决这个问题,这违反了全序的基本定律。)
与jq一样,jaq将nan和infinite打印为null,因为JSON不支持将这些值作为数字进行编码。
分数数的保留
jaq完美地保留了来自JSON数据的分数数(只要它们不用于某些算术运算),而jq 1.6可能静默地转换为64位浮点数
$ echo '1e500' | jq '.'
1.7976931348623157e+308
$ echo '1e500' | jaq '.'
1e500
因此,与jq 1.6不同,jaq满足jq手册中的以下段落
关于身份过滤器的一个重要观点是,它保证了保留值的字面十进制表示。当处理不能无损转换为IEEE754双精度表示的数字时,这一点尤为重要。
请注意,jq的新版本,例如1.7,似乎也保留了字面十进制表示。
赋值
与jq一样,jaq允许以下形式的赋值:p |= f。然而,jaq对这些赋值的解释不同。幸运的是,在大多数情况下,结果是一样的。
在jq中,赋值p |= f首先构造匹配p的所有值的路径。 然后,它将这些值应用于过滤器f。
在jaq中,赋值p |= f立即将f应用于任何匹配p的值。与jq不同,赋值不会显式构造路径。
jaq的赋值实现可能产生更高的性能,因为它不构造路径。此外,这也防止了jq中“设计”的几个错误。例如,给定过滤器[0, 1, 2, 3] | .[] |= empty,jq产生[1, 3],而jaq产生[]。这里发生了什么?
jq首先构建与.[]对应的路径,它们是.0, .1, .2, .3。然后,它会移除这些路径中的每个元素。然而,这些移除都会改变剩余路径所引用的值。也就是说,在移除.0(值0)后,.1不再指向值1,而是值2!这也是为什么值1(以及随之而来的值3)没有被移除的原因。
在jq中还有更多奇怪的情况;例如,0 | 0 |= .+1在jq中产生1的结果,尽管0不是一个有效的路径表达式。然而,1 | 0 |= .+1会产生一个错误。在jaq中,任何这样的赋值都会产生错误。
jaq尝试使用右侧表达式的多个输出,而jq只使用第一个。例如,0 | (., .) |= (., .+1)在jaq中产生0 1 1 2的结果,而在jq中只产生0。然而,{a: 1} | .a |= (2, 3)在jaq和jq中都产生{"a": 2}的结果,因为一个对象只能将单个值与任何给定的键关联起来,所以我们不能在这里以有意义的方式使用多个输出。
由于jaq不构建路径,它不允许在赋值左侧使用某些过滤器,例如 first、last、limit 等:例如,[1, 2, 3] | first(.[]) |= .-1 在jq中会得到 [0, 2, 3],但在jaq中是无效的。同样,[1, 2, 3] | limit(2; .[]) |= .-1 在jq中会得到 [0, 1, 3],但在jaq中是无效的。(顺便一提,jq也不允许使用 last。)
定义
与jq类似,jaq允许定义过滤器,例如
def map(f): [.[] | f];
参数也可以通过值传递,例如
def cartesian($f; $g): [$f, $g];
过滤器定义可以是嵌套和递归的,即引用自身。也就是说,可以在jaq中定义一个如 recurse 的过滤器
def recurse(f): def r: ., (f | r); r;
从jaq 1.2开始,jaq像jq一样优化尾调用。从jaq 1.1开始,递归过滤器也可以有非变量参数,就像在jq中一样。例如
def f(a): a, f(1+a);
具有非变量参数的递归过滤器会产生令人惊讶的效果;例如,一个调用 f(0) 会构建形如 f(1+(..(1+0)...)) 的调用,这会导致指数级执行时间。
具有非变量参数的递归过滤器通常可以通过以下方式实现
- 嵌套过滤器:例如,而不是使用
def walk(f): (.[]? |= walk(f)) | f;,你可以使用def walk(f): def rec: (.[]? |= rec) | f; rec;。 - 带有可变参数的过滤器:例如,您可以使用以下方式替代
def f(a): a, f(1+a);,同样可以写成def f($a): $a, f(1+$a);。 - 带有
recurse的过滤器:例如,您可能编写def f(a): a | recurse(1+.);。如果您期望过滤器深度递归,建议使用recurse实现,因为 jaq 对recurse有优化的实现。
jaq 支持所有这些选项。
参数
与 jq 类似,jaq 允许通过命令行定义参数,特别是通过选项 --arg、--rawfile、--slurpfile。这种方式将变量绑定到值,对于通过这种方式绑定到 $x 的每个变量 v,$ARGS.named 包含一个键为 x、值为 v 的条目。例如
$ jaq -n --arg x 1 --arg y 2 '$x, $y, $ARGS.named'
"1"
"2"
{
"x": "1",
"y": "2"
}
折叠
jq 和 jaq 提供过滤器 reduce xs as $x (init; f) 和 foreach xs as $x (init; f)。
在 jaq 中,这些过滤器的输出定义非常简单:假设 xs 评估为 x0、x1、...、xn,reduce xs as $x (init; f) 评估为
init
| x0 as $x | f
| ...
| xn as $x | f
以及 foreach xs as $x (init; f) 评估为
init
| x0 as $x | f | (.,
| ...
| xn as $x | f | (.,
empty)...)
此外,jaq 还提供了过滤器 for xs as $x (init; f),其评估结果为
init
| ., (x0 as $x | f
| ...
| ., (xn as $x | f
)...)
foreach 和 for 的区别在于,for 会返回 init 的输出,而 foreach 则省略了它。例如,foreach (1, 2, 3) as $x (0; .+$x) 返回 1, 3, 6,而 for (1, 2, 3) as $x (0; .+$x) 返回 0, 1, 3, 6。
在 jaq 中对 reduce/foreach 的解释具有以下优点,优于 jq
-
它非常自然地处理产生多个输出的过滤器。相比之下,jq 在处理
f的输出时有所区分,因为它只对最后一个进行递归,尽管它输出所有这些。示例
foreach (5, 10) as $x (1; .+$x, -.)在 jq 中得到6, -1, 9, 1,而在 jaq 中得到6, 16, -6, -1, 9, 1。我们可以看到,jq 和 jaq 都产生了第一次迭代(其中$x为 5)的结果,即1 | 5 as $x | (.+$x, -.)。然而,jq 仅在第一次迭代返回的最后一个值(即$x为 10)上执行第二次迭代,即-1,得到9和1,这些值来自于-1 | 10 as $x | (.+$x, -.)。jaq 也得到这些值,但它也对第一次迭代返回的所有其他值执行第二次迭代,即6,得到16和-6,这些值来自于6 | 10 as $x | (.+$x, -.)。 -
这使得
reduce和foreach成为同一代码的特殊情况,从而降低了出错的可能性。
与 foreach ... 相比,过滤器 for ...(其中 ... 指的是 xs as $x (init; f))与 reduce 的关系更强。特别是,reduce ... 产生的值是 for ... 产生的值的子集。如果你将 for 替换为 foreach,则这种情况不成立。
示例
例如,如果我们设置 ... 为 empty as $x (0; .+$x),那么 foreach ... 不会产生任何值,而 for ... 和 reduce ... 会产生 0。
此外,jq 提供了过滤器 foreach xs as $x (init; f; proj)(即 foreach/3)并将 foreach xs as $x (init; f; .)(即 foreach/2)解释为 foreach xs as $x (init; f; .),而 jaq 则不提供 foreach/3,因为它需要在解析器和解释器中完全独立的逻辑。
错误处理
在 jq 中,表达式 try f catch g 在出现错误时立即退出 f 流,并在之后将控制权交给 g。这在其手册中作为退出循环的可能机制被提及(见此处)。然而,jaq 不中断 f 流,而是将每个错误值发送到 g 过滤器;结果是来自 f 的值和来自 g 的值在错误发生的位置交错。
考虑以下示例:这个表达式在 jq 中是 true,因为第一个 error(2) 中断了流
[try (1, error(2), 3, error(4)) catch .] == [1, 2]
然而,在 jaq 中,这成立
[try (1, error(2), 3, error(4)) catch .] == [1, 2, 3, 4]
杂项
- 吸入:当通过
-s/--slurp选项吸入文件时,jq将所有文件的输入合并成一个单独的数组,而jaq为每个文件生成一个数组。jaq可以近似实现jq的行为;例如,要实现jq -s . a b的输出,可以使用jaq -s . <(cat a b)。 - 笛卡尔积:在jq中,
[(1,2) * (3,4)]的结果是[3, 6, 4, 8],而[{a: (1,2), b: (3,4)} | .a * .b]的结果也是[3, 4, 6, 8]。jaq在这两种情况下都生成[3, 4, 6, 8]。 - 索引
null:在jq中,当给定null输入时,.["a"]和.[0]的结果是null,但.[]的结果是错误。jaq在所有情况下都会产生错误,以防止意外索引null值。为了在jq和jaq中获得相同的行为,可以使用.["a"]? // null或.[0]? // null代替。 - 列表更新:在jq中,
[0, 1] | .[3] = 3的结果为[0, 1, null, 3];即当更新超出列表大小时,jq会使用null填充列表。相比之下,在这种情况下,jaq会因为越界错误而失败。 - 读取输入:当没有更多的输入值时,在jq中,
input会产生一个错误,而在jaq中,它则不产生输出值。 - 连接:当给定的数组为
[x0, x1, ..., xn]时,在jq中,join(x)将输入数组的所有元素转换为字符串,并用x分隔它们,而在jaq中,join(x)则简单地计算x0 + x + x1 + x + ... + xn。当输入数组的所有元素和x都是字符串时,jq和jaq产生相同的输出。
贡献
欢迎为jaq做出贡献。请确保在您的更改后,cargo test可以成功运行。
致谢
本项目通过NGI0 Entrust基金获得资金支持,该基金由NLnet设立,并获得欧洲委员会下一代互联网项目网络通信、内容和技术总司的财政支持,合同编号No 101069594。
jaq还受益于
- serde_json来读取和colored_json来输出JSON,
- chumsky来解析和ariadne来格式化解析错误,
- mimalloc来提升内存分配的性能,以及
- Rust标准库,特别是其令人惊叹的迭代器,它是jaq过滤器执行坚实的基础。
依赖关系
~2.2–3.5MB
~61K SLoC