使用 jq 将 JSON 数组转换为 bash 变量

使用 jq 将 JSON 数组转换为 bash 变量

我有一个 JSON 数组,如下所示:

{
  "SITE_DATA": {
    "URL": "example.com",
    "AUTHOR": "John Doe",
    "CREATED": "10/22/2017"
  }
}

我希望使用 jq 迭代这个数组,这样我就可以将每个项目的键设置为变量名称,将值设置为它的值。

例子:

  • 网址=“example.com”
  • 作者=“约翰·多伊”
  • 创建=“2017 年 10 月 22 日”

到目前为止我所得到的迭代数组但创建一个字符串:

constants=$(cat ${1} | jq '.SITE_DATA' | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]")

哪个输出:

URL=example.com
AUTHOR=John Doe
CREATED=10/22/2017

我希望在脚本中进一步使用这些变量:

echo ${URL}

但这与目前的空输出相呼应。我猜我需要一个eval或一些东西在那里,但似乎无法将我的手指放在上面。

答案1

您的原始版本将无法运行,eval因为作者姓名中包含空格 - 它将被解释为运行Doe环境变量AUTHOR设置为 的命令John。实际上也不需要通过管道传输jq到自身 -内部管道和数据流可以将不同的过滤器连接在一起。

仅当您完全信任输入数据(例如,它是由您控制的工具生成的)时,所有这一切才有意义。下面详细介绍了几个可能的问题,但我们假设数据本身肯定采用您目前期望的格式。

您可以制作一个更简单的 jq 程序版本:

jq -r '.SITE_DATA | to_entries | .[] | .key + "=" + (.value | @sh)'

其输出:

URL='example.com'
AUTHOR='John Doe'
CREATED='10/22/2017'

不需要map.[]处理将数组中的每个对象作为单独的项目通过管道的其余部分,因此最后一个之后的所有内容|都会分别应用于每个。最后,我们只需用普通+连接组装一个有效的 shell 赋值字符串,包括适当的引号并在值周围转义@sh

所有管道在这里都很重要 - 没有它们,您会得到相当无用的错误消息,其中程序的各个部分在微妙不同的上下文中进行评估。

这个字符串eval可以如果您完全信任输入数据并具有您想要的效果:

eval "$(jq -r '.SITE_DATA | to_entries | .[] | .key + "=" + (.value | @sh)' < data.json)"
echo "$AUTHOR"

与使用时一样eval,请小心您信任所获取的数据,因为如果数据是恶意的或只是采用意外的格式,则可能会出现严重错误。特别是,如果钥匙包含 shell 元字符(如$或 空格),这可以创建一个正在运行的命令。例如,它还可能PATH意外覆盖环境变量。

如果您不信任数据,则要么根本不执行此操作,要么先过滤对象以仅包含您想要的键:

jq '.SITE_DATA | { AUTHOR, URL, CREATED } | ...'

如果以下情况,您也可能会遇到问题价值是一个数组,所以.value | tostring | @sh会更好 - 但这个警告列表可能是一个很好的理由不是首先要做这一切。


也可以建立一个关联数组相反,键和值都被引用:

eval "declare -A data=($(jq -r '.SITE_DATA | to_entries | .[] | @sh "[\(.key)]=\(.value)"' < test.json))"

之后,${data[CREATED]}包含创建日期等,无论键或值的内容是什么。这是最安全的选项,但不会导致可以导出的顶级变量。当值是数组时,它仍然可能会产生 Bash 语法错误;如果是对象,它仍然可能会产生 jq 错误,但不会执行代码或覆盖任何内容。

答案2

基于@Michael Homer 的答案,您可以eval通过将数据读入关联数组来完全避免潜在的不安全。

例如,如果您的 JSON 数据位于名为 的文件中file.json

#!/bin/bash

typeset -A myarray

while IFS== read -r key value; do
    myarray["$key"]="$value"
done < <(jq -r '.SITE_DATA | to_entries | .[] | .key + "=" + .value ' file.json)

# show the array definition
typeset -p myarray

# make use of the array variables
echo "URL = '${myarray[URL]}'"
echo "CREATED = '${myarray[CREATED]}'"
echo "AUTHOR = '${myarray[URL]}'"

输出:

$ ./read-into-array.sh 
declare -A myarray=([CREATED]="10/22/2017" [AUTHOR]="John Doe" [URL]="example.com" )
URL = 'example.com'
CREATED = '10/22/2017'
AUTHOR = 'example.com'

答案3

刚刚意识到我可以循环结果并评估每次迭代:

constants=$(cat ${1} | jq '.SITE_DATA' | jq -r "to_entries|map(\"\(.key)=\(.value|tostring)\")|.[]")

for key in ${constants}; do
  eval ${key}
done

允许我做:

echo ${AUTHOR}
# outputs John Doe

答案4

我真的很喜欢@Michel的建议。有时,您实际上可能只是提取一些变量值来使用 Bash 在该特定服务器中执行任务。因此,如果所需的变量已知,使用这种方法可以避免多次调用来jq设置每个变量的值,甚至可以使用read具有多个变量的语句,其中某些变量可以是有效的,也可以是空的,从而导致值偏移(即我的问题)。

我之前的方法会导致值转换错误 - 如果 .svID[ ].ID="",sv将得到槽位号价值。

-rd '\n' getInfo sv slotID <<< $(jq -r '(.infoCMD // "no info"), (.svID[].ID // "none"), (._id // "eeeeee")' <<< $data)

如果您使用curl 下载对象,下面是我将一些变量重命名为友好名称的方法,以从数据数组中提取数据。

使用 eval 和过滤器只需一行即可解决问题,并生成具有所需名称的变量。

eval "$(jq -r '.[0] | {varNameExactasJson, renamedVar1: .var1toBeRenamed, varfromArray: .array[0].var, varValueFilter: (.array[0].textVar|ascii_downcase)} | to_entries | .[] | .key + "=\"" + (.value | tostring) + "\""' <<< /path/to/file/with/object )"  

这种情况下的优点是,它将在第一步中过滤、重命名、格式化所有所需的变量。观察其中有 .[0] |如果源来自使用 GET 的 RESTful API 服务器,则响应数据如下:

[{"varNameExactasJson":"this value", "var1toBeRenamed: 1500, ....}]

如果您的数据不是来自数组,即是一个类似以下的对象:

{"varNameExactasJson":"this value", "var1toBeRenamed: 1500, ....}

只需删除初始索引:

eval "$(jq -r '{varNameExactasJson, renamedVar1: .var1toBeRenamed, varfromArray: .array[0].var, varValueFilter: (.array[0].textVar|ascii_downcase)} | to_entries | .[] | .key + "=\"" + (.value | tostring) + "\""' <<< /path/to/file/with/object )"

相关内容