13个版本 (4个稳定版)
1.0.3 | 2024年7月30日 |
---|---|
1.0.2 | 2024年1月22日 |
1.0.1 | 2023年11月6日 |
1.0.0-alpha | 2023年4月17日 |
0.7.0 | 2022年6月22日 |
#37 in #jq
25,300 monthly downloads
在 13 个crate中使用 (7 个直接使用)
145KB
2K SLoC
jaq
jaq(发音类似于 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 的二进制文件。
您也可以在 macOS 或 Linux 上使用 homebrew 安装 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 的性能。空基准测试运行 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]:从它们的 GitHub 发布页面获取了 jq-1.7.1 和 gojq-0.12.15 的二进制文件,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 .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位浮点数(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在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 中的几个“按设计”的 bug。例如,给定过滤器 [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只使用第一个。例如,在jaq中,以下代码0 | (., .) |= (., .+1)
会生成0 1 1 2
,而在jq中只会生成0
。然而,在jaq和jq中,以下代码{a: 1} | .a |= (2, 3)
都会生成{"a": 2}
,因为一个对象只能与任何给定的键关联一个值,所以我们不能在这里有意义地使用多个输出。
由于jaq不构建路径,它不允许在赋值左侧进行某些筛选,例如first
、last
、limit:例如,以下代码在jq中会生成
[1, 2, 3] | first(.[]) |= .-1
,但在jaq中是无效的。同样,以下代码在jq中会生成[1, 2, 3] | limit(2; .[]) |= .-1
,但在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
。这会将变量绑定到值,对于通过这种方式绑定到 v
的每个变量 $x
,$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 ...
产生的值的子集。如果用 foreach
替换 for
,则不成立。
示例
例如,如果我们设置 ...
为 empty as $x (0; .+$x)
,那么 foreach ...
不产生任何值,而 for ...
和 reduce ...
产生 0
。
此外,jq 提供了过滤器 foreach xs as $x (init; f; proj)(《foreach xs /3》))并解释
foreach xs as $x (init; f; .)(《foreach xs /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则为每个文件产生一个数组。这源于-
/--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进行索引:
null
输入时,在jq中,.["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–3MB
~54K SLoC