13个版本 (4个稳定版)

1.0.3 2024年7月30日
1.0.2 2024年1月22日
1.0.1 2023年11月6日
1.0.0-alpha2023年4月17日
0.7.0 2022年6月22日

#37 in #jq

Download history 4373/week @ 2024-04-26 4409/week @ 2024-05-03 3901/week @ 2024-05-10 4112/week @ 2024-05-17 3762/week @ 2024-05-24 4774/week @ 2024-05-31 4680/week @ 2024-06-07 5100/week @ 2024-06-14 4941/week @ 2024-06-21 5864/week @ 2024-06-28 6315/week @ 2024-07-05 6542/week @ 2024-07-12 6322/week @ 2024-07-19 6791/week @ 2024-07-26 5431/week @ 2024-08-02 5702/week @ 2024-08-09

25,300 monthly downloads
13crate中使用 (7 个直接使用)

MIT 许可证

145KB
2K SLoC

jaq

Build status Crates.io Documentation Rust 1.64+

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 而不是通过定义。

特性

以下是一个概述,总结了

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

欢迎为扩展 jaq 做出贡献。

基础

  • 恒等(.
  • 递归(..
  • 基本数据类型(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
  • 连接(,
  • 普通赋值(=
  • 更新赋值(|=+=-=
  • 交替(//
  • 逻辑(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))
  • 选择(valuesnullsbooleansnumbersstringsarraysobjectsiterablesscalars
  • 转换(tostringtonumber
  • 可迭代过滤器(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,返回一个(浮点数,整数)对。
  • 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来将整数转换为浮点数。您可以通过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在JSON中将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 中的几个“按设计”的 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不构建路径,它不允许在赋值左侧进行某些筛选,例如firstlastlimit:例如,以下代码在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求值结果为x0x1,...,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
)...)

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)产生的值 6-1,即 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 ... 产生的值的子集。如果用 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还受益于

依赖关系

~2.2–3MB
~54K SLoC