19个版本 (7个稳定版)
1.5.1 | 2024年7月30日 |
---|---|
1.4.0 | 2024年5月23日 |
1.2.1 | 2024年1月22日 |
1.2.0 | 2023年11月17日 |
0.1.0 | 2021年5月19日 |
#1536 在 编码
每月下载量 23,798
在 13 个 包(10个直接使用)中使用
285KB
4.5K SLoC
jaq
jaq(发音类似于 Jacques[^jacques])是JSON数据处理工具jq的克隆。jaq旨在支持jq的大多数语法和操作。
您可以在jaq沙盒上在线尝试jaq。沙盒的说明可以在此找到这里。
jaq关注三个目标
-
正确性:jaq旨在提供比jq更正确和可预测的实现,同时在大多数情况下与jq保持兼容。
令人惊讶的jq行为的示例
nan > nan
是 false,而nan < nan
是 true。[[]] | 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 意为“松鼠”,这为这个名称提供了一个很好的 ex post 启发。
安装
二进制文件
您可以在 发布页面 下载 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当前可以做什么的印象。您应该通过用jq替换jaq来获得相同的输出。如果不是,您提交问题将受到赞赏。 :) 语法在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
次带有空输入的过滤器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在tree-flatten
上速度很快,因为它将过滤器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
,它返回(浮点数,整数)对。 -
ilogb
,它返回整数。 -
j0
-
j1
-
lgamma
-
log
-
log10
-
log1p
-
log2
-
logb
-
modf
,它返回(浮点数,浮点数)对。 -
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位浮点数(浮点数)表示任何数字。相比之下,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在JSON中将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)的结果,即6
和-1
,即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
,因为它需要在解析器和解释器中完全独立的逻辑来处理foreach/2
和reduce
。
错误处理
在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为每个文件生成一个数组。这是由-
/--in-place
选项所驱动,该选项无法与jq实现的行为一起工作。jaq可以近似模拟jq的行为;例如,为了实现jq -. a b
的输出,你可以使用jaq -. <(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 建立,并由欧洲委员会的 Next Generation Internet 计划提供资金支持,在 DG Communications Networks, Content and Technology 的指导下,根据协议号 No 101069594。
jaq 还受益于
- serde_json 读取和 colored_json 输出 JSON,
- chumsky 解析和 ariadne 美化打印解析错误,
- mimalloc 提高内存分配的性能,以及
- Rust 标准库,特别是它的神奇的 Iterator,它为 jaq 的过滤执行建立了坚实的基础。
依赖关系
~4.5–7.5MB
~127K SLoC