Back to prev

jq in action

May 11, 2022
Linkang Chan
@Jesse Chan

日常编写 bash 脚本处理 json 时,我们大概率会使用 jq 这个工具,此时我们需要学习一些 jq 的语法,下面总结一些使用过程中遇到的一些问题和解决方式。

使用 bash 变量

$ var1="hello"
$ jq --arg var1 $var1 '.res[$var1]' filename.json

可以多次使用--arg inner_var bash_var的方式。

检查 key 是否存在

$ jq -r '.result | has("keyname")' filename.json
true  # or false

检查字段是否存在某些字段

$ jq -r '.result.var | contains("content")' filename.json
true # or false

获取 object 的 key 或者 value

比如有如下的 json :

{
  "key1": {
    "key11": {
      "key111": {
        "key1111": "hello"
      }
    }
  },
  "key2": {
    "key22": {
      "key222": {
        "key2222": "world"
      }
    }
  }
}

若只想输入两个 value 的值,helloworld。 可以用的方式是:

$ jq -r '.[] | .[] | .[] | to_entries | .[].value' xxx.json
hello
world

注意这边的to_entries是将内容转变成固定格式:

[
  {
    "key": "key111",
    "value": {
      "key1111": "hello"
    }
  }
]
[
  {
    "key": "key222",
    "value": {
      "key2222": "world"
    }
  }
]

之后再统一取指定的 key 下面的内容。

jq 读取 array 内容到 bash 变量

我们经常需要将 json 的数组内容读取到 bash 的数组中。可以有两种方式,主要看 bash 的版本:

  • bash 4+
$ mapfile -t arr < <(jq -r 'keys[]' xxx.json)
  • older bash
arr=()
while IFS='' read -r line; do
   arr+=("$line")
done < <(jq 'keys[]' xxx.json)

此外还有一种方式用于纯 json 数组的转换,比如:

#[
#    1, 2, 3, 4, 5, 6, 7, 8, 9
#]

$ arr=($(jq -r '. | @sh' xxx.json))

@sh: The input is escaped suitable for use in a command-line for a POSIX shell. If the input is an array, the output will be a series of space-separated strings.

jq 输出多字段内容读取到 bash 变量中

当我们同时需要读取两个并列的 json 字段的内容到脚本中同时处理时,这个过程稍微有点复杂。

 jq -r '. | [.name,.value] | @tsv' model_info.json | \
        while IFS=$'\t' read -r name ctx; do
           echo $name, $ctx
        done

@tsv: The input must be an array, and it is rendered as TSV (tab-separated values). Each input array will be printed as a single line. Fields are separated by a single tab (ascii 0x09). Input characters line-feed (ascii 0x0a), carriage-return (ascii 0x0d), tab (ascii 0x09) and backslash (ascii 0x5c) will be output as escape sequences \n, \r, \t, \ respectively.

总体的思路就是先转成 tsv 格式,后续在 read 指定分隔符读取。

join 指定字段内容

join 是使用指定分隔符去合并相应的数组内容,比如有数组:[1,2,3,4,5]

$ jq -r '.|join("|")' arr.json
1|2|3|4|5

create JSON from associative array

这种情况下我们可以使用 reduce 进行创建,具体操作如下:

#!/bin/bash
# Requires bash with associative arrays
declare -A dict

dict["foo"]=1
dict["bar"]=2
dict["baz"]=3

for i in "${!dict[@]}"
do
    echo "$i" 
    echo "${dict[$i]}"
done |
jq -n -R 'reduce inputs as $i ({}; . + { ($i): (input|(tonumber? // .)) })'

可以看到 reduce 可以将输入的内容切分成 key 和 value,然后按照 key 创建一个新的 json 对象。这边涉及到几个 jq 的语法:

  • ?是一个错误规避或者是可选的的操作
  • //表示 Alternative operator, 可选操作符

|(tonumber? // .)表示将 value 转换成数字,如果 value 转换失败了,则使用原来的 value。

一个比较完整的且安全的函数,可以有如下的封装:

assoc2json() {
    declare -n v=$1
    printf '%s\0' "${!v[@]}" "${v[@]}" |
    jq -Rs 'split("\u0000") | . as $v | (length / 2) as $n | reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])'
}

assoc2json dict

slurp

如果是一个比较简单的场景,我们可以通过-s/--slurp来生成一个数组对象。比如:

$ echo 1 2 3 | jq -s 
#[
#  1,
#  2,
#  3
#]

或者搭配一些内置的函数使用:

$ echo 1 2 3 | jq -s 'add'
# 6

reduce

reduce 在有大量数据输入时效率会优于 slurp 的方式。 这是一个非常高效的方式去做一些处理。

for ((i=0; i<10; i++));do          
echo $i 
done | jq -n 'reduce inputs as $l (0; . +($l))'

#45