从Bash关联数组构建JSON对象

11

我想将bash中的关联数组转换为JSON哈希/字典。我希望使用JQ来完成这个过程,因为它已经是一个依赖项,我可以依赖它来生成格式良好的json。能否有人演示一下如何实现这一点?

#!/bin/bash

declare -A dict=()

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

for i in "${!dict[@]}"
do
    echo "key  : $i"
    echo "value: ${dict[$i]}"
done

echo 'desired output using jq: { "foo": 1, "bar": 2, "baz": 3 }'
6个回答

15

有许多可能性,但考虑到您已经编写了一个bash for循环,您可能会喜欢从您的脚本的这个变体开始:

#!/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? // .)) })'

此结果反映了bash for循环产生的键的排序:

{
  "bar": 2,
  "baz": 3,
  "foo": 1
}

通常,基于将键值对逐行输入jq的方法具有很多优点,每个键在一行上,随后是相应的值。以下是一个遵循此通用方案,但使用NUL作为“行尾”字符的解决方案。

将键和值呈现为JSON实体

为了使上述更加通用,最好将键和值表示为JSON实体。在这种情况下,我们可以编写:

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

其他变化

JSON键必须是JSON字符串,因此可能需要一些工作来确保实现所需的从bash键到JSON键的映射。类似的评论适用于从bash数组值到JSON值的映射。处理任意bash键的一种方法是让jq进行转换:

printf "%s" "$i" | jq -Rs .

当然,您也可以使用bash数组值做同样的事情,并让jq检查值是否可以转换为数字或其他所需的JSON类型(例如使用fromjson? // .)。

一种通用解决方案

这是一个通用解决方案,沿用了jq FAQ中提到的方法,并得到@CharlesDuffy的支持。它使用NUL作为分隔符将bash键和值传递给jq,并且只需要调用一次jq即可。如果需要,过滤器fromjson? // .可以省略或替换为另一个过滤器。

declare -A dict=( [$'foo\naha']=$'a\nb' [bar]=2 [baz]=$'{"x":0}' )

for key in "${!dict[@]}"; do
    printf '%s\0%s\0' "$key" "${dict[$key]}"
done |
jq -Rs '
  split("\u0000")
  | . as $a
  | reduce range(0; length/2) as $i 
      ({}; . + {($a[2*$i]): ($a[2*$i + 1]|fromjson? // .)})'

输出:

{
  "foo\naha": "a\nb",
  "bar": 2,
  "baz": {
    "x": 0
  }
}

好的,我喜欢这种只使用换行分隔输入一次的方法,似乎更通用。我有点困惑 --null-input 和 --raw-input 如何交互,现在正在阅读 reduce 的文档。我认为这应该是被接受的答案。 - htaccess
对于第二种解决方案,其中写有“对于当前情况”,它仅在值为整数时有效。以防万一有人像我一样复制粘贴并尝试将其用于另一个情况。 - Dominic108

5
这个答案来自freenode上的nico103在jq频道中的发言:
#!/bin/bash

declare -A dict=()

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

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

1
虽然不可避免地使用了一堆特定于bash的语言,但为了避免传播shell本地惯用语,我建议使用符合POSIX标准的函数声明语法。assoc2json() {没有使用function关键字,避免依赖ksh语法仅为了与bash兼容。 - Charles Duffy
嗯。顺便提一下,让我有点担心的是,%qeval 风格的消耗 由 bash 引用字符串。我非常确定,在这里处理的大量输入将产生一些不能评估回原始文字值的东西。另一方面,采用我的答案中采取的 printf '%s\0' 方法会非常简单,将其与此处的更短 jq 代码结合起来(只需采用字符串分割位)... - Charles Duffy
如果您看到我(nico103)下面写的答案,我是想让读者自己去取消引号/反转义。这应该很容易(虽然可能不是一行代码)。 - user2259432
有趣的是 printf '%s\0'!谢谢这个想法!jq会将NUL转换为\u0000,所以你需要在它上面分割,但是...你可以!因此,一行代码现在更简单了:printf '%s\0' "${!arrayvar[@]}" "${arrayvar[@]}" | jq -Rs 'split("\u0000") | . as $v | (length / 2) as $n | reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])' - user2259432
根据Charles Duffys的建议,将函数声明语法更改为符合POSIX标准。 - htaccess
显示剩余8条评论

4

您可以将变量初始化为一个空对象{},并在每次迭代中添加键/值{($key):$value},将结果重新注入同一变量:

#!/bin/bash

declare -A dict=()

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

data='{}'

for i in "${!dict[@]}"
do
    data=$(jq -n --arg data "$data" \
                 --arg key "$i"     \
                 --arg value "${dict[$i]}" \
                 '$data | fromjson + { ($key) : ($value | tonumber) }')
done

echo "$data"

2
不错的解决方案,我喜欢将数据作为 jq 参数传递并迭代扩展的方式。 - htaccess
高度可读。 - Chris

3

bash 5.2 引入了 @k 参数转换,使这个过程变得更加容易。例如:

$ declare -A dict=([foo]=1 [bar]=2 [baz]=3)
$ jq -n '[$ARGS.positional | _nwise(2) | {(.[0]): .[1]}] | add' --args "${dict[@]@k}"
{
  "foo": "1",
  "bar": "2",
  "baz": "3"
}

1
不错,也许几年后当这个版本更普及时,需要将其设为被接受的答案。 - htaccess
这个版本给了我一个错误:
./echo-args.sh: line 23: ${args[@]@k}: bad substitution
而被接受的答案“通用解决方案”没有出现错误并正确输出了我的参数:
{ "9": "-s", "8": "alpha1", "7": "-e", "6": "foo", "5": "-b", "4": "foo-foo-foo-foo-01", "3": "-r", "2": "foo-foo-foo-foo-01", "1": "-n", "20": "fooFoo", "18": "foo", "19": "-f", "12": 9092, "13": "-c", "10": "foo-alpha1", "11": "-p", "16": "alpha2.dev2.foo.io", "17": "-l", "14": "", "15": "-h" }
- Darrell
@Darrell,它说的是bash-5.2,你的版本比较旧。 - oguz ismail
@oguzismail 是的,抱歉,我已经取消了踩一下。但是 Stack Overflow 不允许我取消踩一下 :-( - Darrell
1
@Darrell 没关系。如果你想的话,现在可以删除它了。祝你有美好的一天。 - oguz ismail

1

这篇文章是由IRC上的nico103发布的,也就是说,是我。

让我感到困扰的是,这些关联数组的键和值需要加引号。以下是一个开始,需要进一步的工作来取消引号:

function assoc2json {
    typeset -n v=$1
    printf '%q\n' "${!v[@]}" "${v[@]}" |
        jq -Rcn '[inputs] |
                . as $v |
                (length / 2) as $n |
                reduce range($n) as $idx ({}; .[$v[$idx]]=$v[$idx+$n])'
}


$ assoc2json a
{"foo\\ bar":"1","b":"bar\\ baz\\\"\\{\\}\\[\\]","c":"$'a\\nb'","d":"1"}
$

现在需要的是一个jq函数,它可以去掉引号,这些引号有几种类型:
- 如果字符串以单引号(ksh)开头,则以单引号结尾,需要将其删除。 - 如果字符串以美元符号和单引号开头并以双引号结尾,则需要删除它们并取消转义内部反斜杠转义字符。 - 否则保留不变。
最后一项留给读者作为练习。请注意,我在这里使用printf作为迭代器!

0
$ declare -A d=([$'foo\naha']=$'a\nb' [bar]=2 [baz]=$'{"x":0}')
$ a2j='$ARGS.positional | [.[:$n], .[$n:]] | transpose | map({ (first): last }) | add'
$ jq -nc "$a2j" --argjson n ${#d[@]} --args "${!d[@]}" "${d[@]}"
{"bar":"2","baz":"{\"x\":0}","foo\naha":"a\nb"}

这个组合任意的bash键通过peak,使用jq arg通过bertrand martel,但是使用${#d[@]}${!d[@]}${d[@]}来获取数组中的元素数量,键和值,而不是通过${d[@]@k}来获取,由oguz ismail提供。 这样可以使用简单的jq表达式,而不需要_nwise

虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外背景信息可以提高其长期价值。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接