jq 1.5 - 更改现有元素或添加新元素(如果不存在)

时间:2018-02-27 17:29:31

标签: json traversal jq

使用:

目标和条件:

  1. 将子对象值替换为具有父对象或数组的任何深度的其他值,例如:
    • if .spec.template.spec.containers [ n ]。env [ n ] .name ==" CHANGEME& #34;然后
    • .spec.template.spec.containers [ n ]。env [ n ] .value =" xx&#34 ;
    • 其中 n > = 0
  2. 如果 .name 的任何父母不存在,则应该能够动态添加它们而不是退出并显示错误
  3. 输出JSON应至少具有与输入JSON相同的元素,不应丢失现有元素
  4. 数组元素中不允许重复,但必须保留顺序,因此无法使用unique等函数
  5. 示例输入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:"&# 34;}。预期的输出应为:

      [
        {
          "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 存在,则
          • 有效,否则:
          • 错误:尝试访问元素" env1"附近的路径表达式无效{" name":" cassandra"," env" ..
          • 如果使用.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)'
          • 这可以正常工作,但是太长而且太重,必须在对象层次结构中的每个潜在数组之后添加自定义函数
    3. 问题

      有没有办法简化所有这些,让它更通用地处理任意数量的父,数组或不?

      谢谢。

2 个答案:

答案 0 :(得分:1)

&#34;问题&#34;

回答问题:是的。 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 FAQ https://github.com/stedolan/jq/wiki/FAQ等。

&#34;数组元素中不允许重复&#34;

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

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

&#34;如果.name的任何父母不存在,应该能够动态添加#34;

如果我正确理解了这个要求,你就会知道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 :(得分:0)

管理以达到一个非常好的形式:

  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)'
    
  3. 结果将是所有不存在的对象都将被创建。这里的约定是对你想要成为数组的每个对象使用arr函数,最后使用布尔条件和对象来替换匹配的对象或添加到父数组(如果不匹配)

    1. 如果您知道路径始终存在,那么您要更新的对象也是如此,walk更优雅:

      jq -r 'map(updobj(select(.name=="CHANGEME").value|="xx"))'
      
    2. 感谢@peak的努力并激励解决方案。