【问题标题】:How to recursively merge inherited json array elements?如何递归合并继承的json数组元素?
【发布时间】:2025-11-21 09:45:02
【问题描述】:

我有以下名为 CMakePresets.json 的 json 文件,它是一个 cmake 预设文件:

{
  "configurePresets": [
    {
      "name": "default",
      "hidden": true,
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/_build/${presetName}",
      "cacheVariables": {
        "YIO_DEV": "1",
        "BUILD_TESTING": "1"
      }
    },
    {
      "name": "debug",
      "inherits": "default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "default",
      "binaryDir": "${sourceDir}/_build/Debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    },
    {
      "name": "arm",
      "inherits": "debug",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/Toolchain/arm-none-eabi-gcc.cmake"
      }
    }
  ]
}

我想递归地与 * 合并 configurePresets 元素,这些元素为特定条目 name 继承自身。我有一个名为arm 的节点示例,并且希望生成具有已解析继承的json 对象。父元素的名称存储在每个元素的 .inherits 中。 arm 继承于 debug,后者继承于 default

Remove a key:value from an JSON object using jqthis answer 的帮助下,我可以编写一个我认为可行的 bash shell 循环:

input=arm
# extract one element
g() { jq --arg name "$1" '.configurePresets[] | select(.name == $name)' CMakePresets.json; };
# get arm element
acc=$(g "$input");
# If .inherits field exists
while i=$(<<<"$acc" jq -r .inherits) && [[ -n "$i" && "$i" != "null" ]]; do
   # remove it from input
   a=$(<<<"$acc" jq 'del(.inherits)');
   # get parent element
   b=$(g "$i");
   # merge parent with current
   acc=$(printf "%s\n" "$b" "$a" | jq -s 'reduce .[] as $item ({}; . * $item)');
done;
echo "$acc"

输出,我认为这是arm 的预期输出:

{
  "name": "arm",
  "hidden": true,
  "generator": "Ninja",
  "binaryDir": "${sourceDir}/_build/${presetName}",
  "cacheVariables": {
    "YIO_DEV": "1",
    "BUILD_TESTING": "1",
    "CMAKE_BUILD_TYPE": "Debug",
    "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/Toolchain/arm-none-eabi-gcc.cmake"
  }
}

但我想写在jq。我试过了,jq 语言对我来说并不直观。例如,我可以为两个(即可数)元素做到这一点:

< CMakePresets.json jq --arg name "arm" '
   def g(n): .configurePresets[] | select(.name == n);
   g($name) * (g($name) | .inherits) as $name2 | g($name2)
'

但是当$item 真的是g($name) 这取决于最后一个g($name) | .inherits 时,我不知道该怎么做reduce .[] as $item ({}; . * $item)。我尝试阅读 jq manual 并了解变量和循环,但 jq 的语法非常不同。我尝试使用while,但这只是我不理解且不知道如何修复的语法错误。我猜whileuntil 可能不在此处,因为它们对先前的循环输出进行操作,而元素始终来自根目录。

$ < CMakePresets.json jq --arg name "arm" 'def g(n): .configurePresets[] | select(.name == n);
while(g($name) | .inherits as $name; g($name))   
'
jq: error: syntax error, unexpected ';', expecting '|' (Unix shell quoting issues?) at <top-level>, line 2:
while(g($name) | .inherits as $name; g($name))                                      
jq: 1 compile error

如何用jq 语言编写这样的循环?

【问题讨论】:

    标签: json inheritance merge jq ancestor


    【解决方案1】:

    假设继承层次结构不包含循环,就像示例中的情况一样,我们可以将问题分解为如下所示的部分:

    # Use an inner function of arity 0 to take advantage of jq's TCO
    def inherits_from($dict):
      def from:
        if .name == "default" then .
        else $dict[.inherits] as $next
        | ., ($next | from)
        end;
      from;
    
    def chain($start):
      INDEX(.configurePresets[]; .name) as $dict
      | $dict[$start] | inherits_from($dict);
    
    reduce chain("arm") as $x (null;
      ($x.cacheVariables + .cacheVariables) as $cv
      | $x + .
      | .cacheVariables = $cv)
    | del(.inherits)
    

    这会有效地产生所需的输出。

    上述解决方案的一个优点是可以很容易地对其进行修改以处理循环依赖关系。

    使用recurse/1

    inherits_from/1也可以使用内置函数recurse/1来定义:

    def inherits_from($dict):
      recurse( select(.name != "default") | $dict[.inherits]) ;
    

    或者更有趣的是:

    def inherits_from($dict):
      recurse( select(.inherits) | $dict[.inherits]) ;
    

    使用*

    使用* 组合对象具有很高的开销,因为它具有递归语义,这通常不是必需的或不需要的。然而, 如果这里可以使用* 来组合对象,则上面可以简化为:

    def inherits_from($dict):
      recurse( select(.inherits) | $dict[.inherits]) ;
    
    INDEX(.configurePresets[]; .name) as $dict
    | $dict["arm"] 
    | reduce inherits_from($dict) as $x ({};  $x * .)
    | del(.inherits)
    

    【讨论】:

      【解决方案2】:

      编写递归函数其实很简单,只要你掌握了窍门:

      jq --arg name "$1" '
          def _get_in(input; n):
              (input[] | select(.name == n)) |
              (if .inherits then .inherits as $n | _get_in(input; $n) else {} end) * .;
          def get(name):
              .configurePresets as $input | _get_in($input; name);
          get($name)
      ' "$presetfile"
      

      首先我只过滤.configurePresets,然后在一个函数中我得到input[] | select(.name == n)只有我感兴趣的部分。然后if .inherits如果它有继承,然后.inherits as $n | _get_in(input; $n)在继承中取名字并再次调用自己。否则返回else {} end 为空。然后就是 * .input[] | select(.name == n) 的结果合并 - 本身。所以它递归地加载所有{} * (input[]|select()) * (input[]|select()) * (input[]|select())

      【讨论】:

      • 这个解决方案相当低效:(a)input[] | select(.name == n) vs 有一个查找表; (b) 在 jq 中,通常最好避免让数量大于 0 的函数直接调用自己。
      • 一个风格点:由于input/0 是一个内置函数,最好不要在def 中使用“输入”作为命名参数。