命令行 JSON 处理工具 jq 的使用介绍(4)

jq 中同样存在变量作用域问题。在 jq 中,有两种方法分隔变量作用域。第一种是用括号包围部分表达式。括号内部的表达式与外部的表达式不在同一个作用域范围内。第二种方法是定义函数。默认情况下,声明的变量对其后的表达式可见。但是,如果变量在特定作用域内声明,则对作用域外部的表达式不可见,例如:

清单 8.变量作用域

1

2

3

4

5

6

7

8

 

#会抛出 arraylen 没定义的异常

jq -nr '[1,2,3]|(length as $arraylen|$arraylen)|$arraylen+1'

 

#正常执行,结果为 4.

jq -nr '[1,2,3]|(length as $arraylen|$arraylen+1)'

 

#函数作用域。该表达式会抛出异常,因为变量$fn 是在函数 fname 中定义,对最后一个子表达式##来说,$fn 是不可见的。

jq -nr '{"firstname":"tom","lastname":"clancy"}|def fname:. as {firstname:$fn, lastname:$ln}|$fn; fname|$fn'

 

Reduce

我们知道 jq 有一种特殊的数据类型:迭代器。通常,有迭代器参与的运算,其结果也是一个迭代器。jq 提供了一些特殊的语法和内置函数用来缩减迭代器运算结果的个数。

reduce 关键字用来通过运算将迭代器的所有值合并为一个值。其调用形式为:reduce <itexp> as $var (INIT; UPDATE)。其中,表达式 itexp 产生的迭代器被赋值给变量 var, UPDATE 是关于变量 var 的表达式。INIT 是该表达式的初始输入。相对于 itexp 结果中的每个元素,UPDATE 表达式被调用一次,计算出结果用作下一次 UPDATE 调用的输入。

清单 9. reduce 关键字

1

2

3

4

 

#结果是 6

jq -nr 'reduce ([1,2,3]|.[]) as $item (0; .+$item)'

#上面的表达式等同于

jq -nr '0 | (3 as $item|.+$item)|(2 as $item | . + $item)|(1 as $item | . + $item)'

 

关键字 foreach 的作用和 reduce 类似。其调用形式为 foreach EXP as $var (INIT; UPDATE; EXTRACT)。和 reduce 关键字不同的是,foreach 关键字的每次迭代是先调用 UPDATE 再调用 EXTRACT,并以一个迭代器保留每一次的中间结果。该迭代器最后作为整个表达式的结果输出。

清单 10. foreach 关键字

1

2

 

#下面的表达式,结果是 1 3 6

jq -nr 'foreach ([1,2,3]|.[]) as $item (0; .+$item;.)'

 

内置函数 limit(n;exp)用来取得表达式 exp 结果的前 n 个值。

内置函数 first, last 和 nth。这几个函数用来取迭代器中某一个特定的元素。这几个函数既可以以函数的形式调用,也可以作为子表达式调用。请看下面的示例:

清单 11. firs, last 和 nth

1

2

3

4

5

6

7

 

#下面的表达式按照函数的形式调用 first,结果为 1

jq -nr 'first([1,2,3]|.[])'

#下面的表达式以 filter 形式调用 first

jq -nr '[1,2,3]|.[]|first'

 

#nth 函数的使用,结果为 2

jq -nr 'nth(1;[1,2,3]|.[])'

 

自定义函数和模块化

作为一个类似于编程语言的表达式系统,jq 也提供了定义函数的能力。其语法规则为:def funcname(arguments) : funcbodyexp; 在定义函数时,需要注意下面几条规则。

函数名或者参数列表后面应该跟冒号以标志函数体开始。

如果不需要参数,可以直接把整个参数列表部分省去。

参数列表中,参数之间以分号(";")分隔。

函数体只能是一个表达式,且表达式需以分号结尾

如果在表达式内部定义函数,整个子表达式部分不能只包含函数定义,否则 jq 会抛出语法错误

在很多情况下,函数的参数都是被当作表达式引用的,类似于编程其他语言中的 callback 函数。

清单 12. map 函数的源代码

1

2

3

 

def map(f): [.[] | f];

#下面表达式的结果是 20,因为当作参数传入的表达式在函数 foo 中被引用两次

5|def foo(f): f|f;foo(.*2)

 

如果希望传入的参数只被当作一个简单的值来使用,则需要把参数的值定义为一个同名变量,并按照使用变量的方式引用。

清单 13. 值参数

1

2

3

4

5

6

7

 

#下面表达式结果为 10,传入的表达式'.*2'在函数 foo 中首先被求值。

5|def foo(f): f as $f|$f|$f;foo(.*2)

#上面的表达式可以简写为如下形式,注意,引用参数时必须带$。

5|def foo($f): $f|$f;foo(.*2)

#否则等于直接引用参数中的表达式。

#例如下面的表达式结果为 20

5|def foo($f): $f|f;foo(.*2)

 

函数内部可以定义子函数。利用这个特性我们可以实现递归函数。

清单 14.递归函数实现数组求和

1

2

 

#下面表达式的结果是 15

jq -nr '[1,2,3,4,5]|def total: def _t: .|first+(if length>1 then .[1:]|_t else 0 end); _t;total'

 

除了在表达式内部定义函数外,我们可以把自定义函数写在外部文件中形成单独的类库。jq 有一套完整的模块系统来支持自定义类库。

首先,可以通过命令行参数'-L'来指定 jq 搜索模块时需要搜索的路径。

其次,在模块内部,可以通过 import 指令和 include 指令来实现互相引用。在引用指令中,有几个特殊的路径前缀需要说明。

'~',表示当前用户的 home 目录

'$ORIGIN'表示 jq 命令的可执行文件所在的目录

'.'表示当前目录,该前缀只能用在 include 指令中。

当通过 import 指令引用一个模块 foo/bar 时, jq 会在搜素路径中查找 foo/bar.jq 或者 foo/bar/bar.jq。

结束语

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wzdxdg.html