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-beta2023 年 8 月 4 日

编程语言 中排名第 8

Download history 4349/week @ 2024-05-03 3758/week @ 2024-05-10 4066/week @ 2024-05-17 3810/week @ 2024-05-24 4615/week @ 2024-05-31 4607/week @ 2024-06-07 5001/week @ 2024-06-14 5147/week @ 2024-06-21 6109/week @ 2024-06-28 6283/week @ 2024-07-05 6476/week @ 2024-07-12 6145/week @ 2024-07-19 6299/week @ 2024-07-26 5395/week @ 2024-08-02 6587/week @ 2024-08-09 5503/week @ 2024-08-16

每月下载量 25,096
12 包(9 个直接)使用

MIT 许可协议

225KB
4K SLoC

jaq

Build status Crates.io Documentation Rust 1.64+

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上要快得多。

特性

以下是一个概述,总结了

  • 已实现的功能,以及
  • 尚未实现的功能。

对扩展jaq的贡献非常欢迎。

基础

  • 身份(.
  • 递归(..
  • 基本数据类型(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
  • 连接(,
  • 直接赋值(=
  • 更新赋值(|=+=-=
  • 替代(//
  • 逻辑(orand
  • 相等和比较(.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)addjoin("a")
  • 数组过滤器(transposefirstlastnth(10)flattenminmax
  • 对象-数组转换(to_entriesfrom_entrieswith_entries
  • 通用/存在性(allany
  • 递归(walk
  • I/O(input
  • 正则表达式(testscanmatchcapturesplitssubgsub
  • 时间(fromdatetodate

数字过滤器

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将整数转换为浮点数,例如。您可以通过roundfloorceil将浮点数转换为整数。

$ 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将naninfinite打印为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不构建路径,它不允许在赋值左侧使用某些过滤器,例如 firstlastlimit 等:例如,[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 评估为 x0x1、...、xnreduce 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
)...)

foreachfor 的区别在于,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,得到 91,这些值来自于 -1 | 10 as $x | (.+$x, -.)。jaq 也得到这些值,但它也对第一次迭代返回的所有其他值执行第二次迭代,即 6,得到 16-6,这些值来自于 6 | 10 as $x | (.+$x, -.)

  • 这使得 reduceforeach 成为同一代码的特殊情况,从而降低了出错的可能性。

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还受益于

依赖关系

~2.2–3.5MB
~61K SLoC