art vanderlay
art vanderlay

Reputation: 2463

jq 1.5 match a record where 2 or more keys match names and values

Not quite sure how to ask the question clearly but, given a recursive structure as below. How would I use walk to match 2 or more key strings to values. I will not know where in the structure the result will be. It could be the top level or 10 levels deep.

"children": {
    "ccc": [{
        "id": "ddd",
        "des": "object d",
        "parent": "ccc",
        "other": "zzz"
    },{
        "id": "zzz",
        "des": "object z",
        "parent": "ccc",
        "other" : "ddd"
  }]
}

I would like to find a record where key=id=ddd && key=parent=cccI would then like to add a new key/value to that record. Using .key|match("") will give me a match to the value of the key but not the key name itself. So searching for ddd may match both id and other.

I have tried several combos and if doing in bash it would look something like

match_criteria

     ((.key|match("id") and (.key|test("ddd")) 
        and 
     ((.key|match("parent") and (.key|test("ccc")) 

new_key_value

+= {"newkey":"newValue"}

insert match statement into

walk(if type == "object"
      then
        with_entries(if ..match_criteria.. )
      then ..new_key_value.. else . end)

so the result should look like

"children": {
    "ccc": [{
        "id": "ddd",
        "des": "object d",
        "parent": "ccc",
        "other": "zzz",
        "newkey": "newValue"
    },{
        "id": "zzz",
        "des": "object z",
        "parent": "ccc",
        "other":"ddd"
  }]
}

UPDATE based on feedback in the answer from @peak i have updated the code as follows

jsonOut=$(jq 'walk(when(type == "object";
              with_entries(
                  when(any(.value[]; .id == "ddd");
                           .value[] += {"newkey": "newValue"}
                            ))))' <<< ${jsonIn})

unfortunately this still leaves two open issues

a) this code adds {"newkey": "newValue"} to all children where the search criteria is true, ie: to both id:ddd && id:zzz, rather than to just the id:ddd record

"children": {
        "ccc": [{
            "id": "ddd",
            "des": "object d",
            "parent": "ccc",
            "other": "zzz",
            "newkey": "newValue"
        },{
            "id": "zzz",
            "des": "object z",
            "parent": "ccc",
            "other":"ddd",
           "newkey": "newValue"
      }]
    }

b) adding multiple section criteria to the any clause. I have tried using the AND or | joining methods but this throws errors.

when(any(.value[]; .id == "ddd" | .other == "zzz"); //no match, no value added
or
when((any(.value[]; .id == "ddd") AND (any(.value[]; .other == "zzz"));
     //error : unexpected ')', expecting $end
or
when(any(.value[]; .id == "ddd", .other == "zzz"); //no match, no value added

Can you advise the syntax for both issues.

UPDATE2 Understanding the when filter a littler better, I have now nested these and it seems to work in narrowing the result set. However problem a) updating both records when a match is true still exists.

jsonOut=$(jq 'walk(when(type == "object";
          with_entries(
                       when(any(.value[]; .id == "ddd");
                        when(any(.value[]; .other == "zzz");
                            .value[] += {"newkey": "newValue"}
                            )))))' <<< ${jsonIn})

jsonIn

{"children": {
    "ccc": [{
        "id": "ddd",
        "des": "object d",
        "parent": "ccc",
        "other": "zzz"
    },{
        "id": "zzz",
        "des": "object z",
        "parent": "ccc",
        "other":"ddd"
  }],
  "www": [{
        "id": "ddd",
        "des": "object d",
        "parent": "www",
        "other": "ppp"
   },{
        "id": "kkk",
        "des": "object z",
        "parent": "www",
        "other":"ddd"
  }]
}}

jsonOut

{
    "children": {
        "ccc": [{
            "id": "ddd",
            "des": "object d",
            "parent": "ccc",
            "other": "zzz",
            "newkey": "newValue"
        }, {
            "id": "zzz",
            "des": "object z",
            "parent": "ccc",
            "other": "ddd",
            "newkey": "newValue" <=need to NOT add this entry
        }],
        "www": [{
            "id": "ddd",
            "des": "object d",
            "parent": "www",
            "other": "ppp"
        }, {
            "id": "kkk",
            "des": "object z",
            "parent": "www",
            "other": "ddd"
        }]
    }
}

Upvotes: 1

Views: 316

Answers (2)

peak
peak

Reputation: 116957

Here is a response to the "UPDATED" question:

walk(when(type == "object";
          with_entries(when(.key|test("ccc");
                            .value |= map( when(.id=="ddd";
                                      . + {"newkey": "newValue"}))))))

p.s.

In future, please follow the mcve guidelines: http://stackoverflow.com/help/mcve

Upvotes: 1

peak
peak

Reputation: 116957

The simplest way to use walk here is to include the update in the "then" part of the if ... then ... else ...end. To emphasize and clarify this, and to shorten the solution, I'll use the generic helper function, when:

def when(filter; action): if (filter?) // null then action else . end;

The solution to the problem can now be written in a very straightforward way:

walk(when(type == "object";
          with_entries(when(.key|test("ccc");
                       when(any(.value[]; .id == "ddd");
                            .value += ["ADDITIONAL"])))))

Of course you might want a fancier test than .id == "ddd", and you might want to perform the update only once per "ccc" object, but the same structure would be used.

In reality, you might also want to wrap the above expression in a def so it can more readily be parameterized and maintained.

Upvotes: 0

Related Questions