【问题标题】:jq 1.5 - Change existing element or add new if does not existjq 1.5 - 更改现有元素或添加新元素(如果不存在)
【发布时间】:2018-02-27 17:29:30
【问题描述】:

使用:

目标和条件:

  1. 将子对象值替换为具有父对象或数组的任意深度的另一个值,例如:
    • 如果 .spec.template.spec.containers[n].env[n].name == "CHANGEME" 那么
    • .spec.template.spec.containers[n].env[n].value = "xx"
    • 其中 n >=0
  2. 如果 .name 的任何父级不存在,应该能够即时添加它们而不是退出并出现错误
  3. 输出 JSON 应至少具有与输入 JSON 相同的元素,不应丢失现有元素
  4. 数组元素内不允许重复,但顺序必须保留,因此不能使用unique等函数

示例输入 JSON:

结构实际上是强加的,所以我必须服从它。对象“路径”通常类似于:.spec.template.spec.containers[0].spec.env[1].name。你也可以有 .containers[1],等等。这是高度可变的,有时某些元素可能存在与否,取决于特定 JSON 的架构定义。

[
  {
    "kind": "StatefulSet",
    "spec": {
      "serviceName": "cassandra",
      "template": {
        "spec": {
          "containers": [
            {
              "name": "cassandra",
              "env": [
                {
                  "name": "CASSANDRA_SEEDS",
                  "value": "cassandra-0.cassandra.kong.svc.cluster.local"
                },
                {
                  "name": "CHANGEME",
                  "value": "K8"
                }
              ]
            }
          ]
        }
      }
    }
  }
]

场景

  1. 在保留输入结构的同时替换现有值,按预期工作:
    • jq -r 'map({name:"CHANGEME",value: "xx"} as $v | (.spec.template.spec.containers[].env[] | select(.name==$v.name))|=$v)'
  2. 假设我也想做同样的事情,只是 .env1 是对象 {name:"",value:""} 的父数组。预期的输出应该是:

    [
      {
        "kind": "StatefulSet",
        "spec": {
          "serviceName": "cassandra",
          "template": {
            "spec": {
              "containers": [
                {
                  "name": "cassandra",
                  "env": [
                    {
                      "name": "CASSANDRA_SEEDS",
                      "value": "cassandra-0.cassandra.kong.svc.cluster.local"
                    },
                    {
                      "name": "CHANGEME",
                      "value": "K8"
                    }
                  ],
                  "env1": [
                    {
                      "name": "CHANGEME",
                      "value": "xx"
                    }
                  ]
                }
              ]
            }
          }
        }
      }
    ]
    
    • 为此,我尝试动态添加一个对象 env1:
      • jq -r 'map({name:"CHANGEME",value: "xx"} as $v | (.spec.template.spec.containers[] | if .env1 == null then .+={env1:[$v]} | .env1 else .env1 end | .[] | select(.name==$v.name))|=$v)'
        • 如果 .env1 存在则有效,否则:
        • 错误:尝试访问 {"name":"cassandra","env"..的元素 "env1" 附近的路径表达式无效。
        • 如果使用 .env//[$v].env//=.env[$v] 等符号,结果相同
      • jq -r 'map({name:"CHANGEME",value: "xx"} as $v | (.spec.template.spec.containers[].env1 | .[if length<0 then 0 else length end]) |= $v)'
        • 如果 .env1 存在则有效
        • 如果数组 .env1 存在,则添加另一个元素,可能会复制对象
    • 最终我设法创建了一个工作过滤器:
      • jq -r 'def defarr: if length<=0 then .[0] else .[] end; def defarr(item): if length<=0 then .[0] else foreach .[] as $item ([]; if $item.name == item then $item else empty end; .) end; map({name:"CHANGEME",value: "xx"} as $v | (.spec.template.spec | .containers1 | defarr | .env1 | defarr($v.name) ) |=$v)'
        • 这按预期工作,但是太长太重,必须在对象层次结构中的每个潜在数组之后添加自定义函数

问题

有什么办法可以简化这一切,让它更通用地处理任意数量的父母、数组吗?

谢谢。

【问题讨论】:

  • Vanilla JS 似乎比这更简单(以nodejs 为例)
  • 我使用 jq 作为 bash 脚本的一部分来生成部署并更改 kubernetes 集群的各种配置。我更喜欢 jq,因为它是轻量级的,并且已经在主要的 Linux 版本中提供。
  • 您对“foo”的规范的问题在于 .parent1.parent2[].parent3.parent4[].parent5[] 并不是真正的路径或路径规范,因为它包含 @ 987654333@。此类表达式很有用,因为它们可用于检索多个值,但如果您将路径视为仅由字符串和整数组成的数组,它可能会有所帮助。这样的数组实际上被 jq 的getpathsetpath 使用,并且有充分的理由。
  • 明白,我实际上会删除那个幻想规格

标签: json traversal jq


【解决方案1】:

“问题”

回答问题:是的。 jq 1.5 有keys_unsorted,因此您可以使用walk/1 的以下def,现在这是jq“主”版本的标准:

# Apply f to composite entities recursively, and to atoms
def walk(f):
  . as $in
  | if type == "object" then
      reduce keys_unsorted[] as $key
        ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
  elif type == "array" then map( walk(f) ) | f
  else f
  end;

更多细节和示例,请参见jq手册的“开发”版本,jq FAQhttps://github.com/stedolan/jq/wiki/FAQ等。

“数组元素内不允许重复”

这很容易使用index/1 完成;您可能希望使用辅助函数,例如:

def ensure_has($x): if index([$x]) then . else . + [$x] end;

“如果 .name 的任何父级不存在,应该能够即时添加它们”

如果我正确理解了这个要求,知道 jq 将基于分配创建对象对您很有用,例如

{} | .a.b.c = 1

产量

{"a":{"b":{"c":1}}}

因此,使用您的示例,您可能希望在 walk 中包含类似的内容:

if type == "object" and has("spec")
   then (.spec.template.spec.containers? // null) as $existing
   | if $existing then .spec.template.spec.containers |= ... 
     else .spec.template.spec.containers = ...
     end
else .
end

【讨论】:

  • 感谢您的回答。 1.我不确定如何使用walk来实现我所需要的。我已经尝试了一点,但我无法弄清楚它的用法,特别是如果某些元素实际上是输入中预期的数组但实际上并不存在(还)。 2.确实,jq可以生成这样的对象,但是结构变化很大,其中一些对象是数组中的对象中的数组,等等,无穷大甚至更远:) 3.采纳你的最后一个建议:如果.template怎么办不存在?还是第二个 .spec 或 .containers(实际上是数组而不是对象类型)?
  • 使用 walk(if type=="object" then (select(.name=="CHANGEME")|.value)|="x" else . end) 会将现有的 {name:"CHANGEME",value:"something"} 更新为 value:"x",但前提是存在。到目前为止,我还没有指定路径,这是一个改进。但是,如果路径中的任何对象丢失,这将返回 null 或 []
  • walk(f) 遍历作为 INPUT 给出的 JSON 实体,并将遇到的每个节点替换为在该节点处评估的 f。所以你必须写 f 来涵盖所有的可能性。与您的示例一样, f 通常采用以下形式:if type == .... and .... then .... else . end
【解决方案2】:

设法达到一个非常好的形式:

  1. ~/.jq中添加了以下功能:

    def arr:
        if length<=0 then .[0] else .[] end;
    
    def arr(f):
        if length<=0 then
            .[0]
        else
            .[]|select(f)
        end//.[length];
    
    def when(COND; ACTION):
        if COND? // null then ACTION else . end;
    
    # Apply f to composite entities recursively, and to atoms
    def walk(f):
      . as $in
      | if type == "object" then
          reduce keys_unsorted[] as $key
            ( {}; . + { ($key):  ($in[$key] | walk(f)) } ) | f
      elif type == "array" then map( walk(f) ) | f
      else f
      end;
    
    def updobj(f):
      walk(when(type=="object"; f));
    
  2. 典型的过滤器如下所示:

    jq -r '{name:"CHANGEME",value: "xx"} as $v |
        map( when(.kind == "StatefulSet";
                  .spec.template.spec.containers|arr|.env|arr(.name==$v.name)) |= $v)'
    

结果将是创建所有不存在的对象。这里的约定是为每个要成为数组的对象使用arr 函数,最后使用布尔条件和对象替换匹配的对象或添加到父数组(如果不匹配)。

  1. 如果您知道路径始终存在,并且您要更新的对象也始终存在,walk 会更优雅:

    jq -r 'map(updobj(select(.name=="CHANGEME").value|="xx"))'
    

感谢@peak 的努力和启发解决方案。

【讨论】:

    猜你喜欢
    • 2014-12-03
    • 1970-01-01
    • 2020-10-25
    • 2021-01-31
    • 2017-11-30
    • 2017-07-03
    • 1970-01-01
    • 2018-07-20
    • 1970-01-01
    相关资源
    最近更新 更多