Timm
Timm

Reputation: 2548

jq get object key and change its name in one filter

I'm working with this JSON (part of a larger JSON code):

group='{ "129": { "bounds": { "left": 20, "top": 20, "width": 250, "height": 200 }, "slot": 88, "userSize": null, "stackTabs": true, "showThumbs": true, "showUrls": true, "tileIcons": true, "catchOnce": true, "catchRules": "", "title": "", "id": 129 } }'

First I get the top-level key (here "129"), which can actually be any number:

key=`echo $group | jq 'keys[0]'` # "129"

I want to change this key to a different number named nextID which I got from another entry in the larger JSON:

nextID=`echo $groups_meta | jq '.nextID'` # 130

Now here is the problem. I am able to change the key with the following filter, but this seems to me rather fragile and complex:

new_group=`echo $group | jq --arg key $key --arg nextID $nextID 'with_entries( if .key | contains($key) then .key |= "$nextID" else . end)'`

You see that the filter finds the key by searching for the name. That may be a problem if there's another key later on with the same name. That's fragile. And this method seems rather complex; I'd really like to get the key (as in keys[0] above) and use that for the change, all in the same filter.

So there's my question: is there a way to combine both parts, and get the key without searching?

UPDATE

Sample inputs:

group='{ "129": { "bounds": { "left": 20, "top": 20, "width": 250, "height": 200 }, "slot": 88, "userSize": null, "stackTabs": true, "showThumbs": true, "showUrls": true, "tileIcons": true, "catchOnce": true, "catchRules": "", "title": "", "id": 129 } }'
nextID=130

jq command:

new_group=`echo $group | jq --arg nexID $nextID 'filter'`

Expected output:

echo $new_group
{ "130": { "bounds": { "left": 20, "top": 20, "width": 250, "height": 200 }, "slot": 88, "userSize": null, "stackTabs": true, "showThumbs": true, "showUrls": true, "tileIcons": true, "catchOnce": true, "catchRules": "", "title": "", "id": 130 } }

Note that the first key "129" should be addressed by position, similar to keys[0] as shown above, as the number is not known beforehand. The value of key id is also set to "130". Both should be set with the bash variable $nextID imported into jq.

UPDATE 2

Using @jq170727's answer I added a few value changes after nextID successfully, but there's an issue changing the .title value in the same input to a string with spaces:

title="new title"
new_group=`echo $group | jq --arg nextID $nextID --arg title $title '(keys_unsorted|first) as $i | with_entries(if .key == $i then .key=$nextID | .value.id=$nextID | .value.title=$title else . end)'`

This throws an error:

jq: error: title/0 is not defined at <top-level>, line 1:
title

Quoting the title variable in the jq command .value.title="$title" also errors out. If title has no spaces in bash, the command works.

Am I missing a trick to update values with variables containing spaces? Should I ask a new question?

Upvotes: 0

Views: 1232

Answers (2)

Timm
Timm

Reputation: 2548

Since the 'group' object has only one top-level key, there's no need to prevent sorting, so @jq170727's revised filter can be simplified quite a bit:

with_entries(.key=$nextID | .value.id=$nextID)

Answer to my update 2:

The imported bash variable needs to be quoted. To prevent errors due to spaces in a value it makes sense to quote all imported variables:

new_group=`echo $group | jq --arg nextID "$nextID" --arg title "$title" 'with_entries(.key=$nextID | .value.id=$nextID | .value.title=$title)'`

Output:

{ "130": { "bounds": { "left": 20, "top": 20, "width": 250, "height": 200 }, "slot": 88, "userSize": null, "stackTabs": true, "showThumbs": true, "showUrls": true, "tileIcons": true, "catchOnce": true, "catchRules": "", "title": "new title", "id": "130" } }

Upvotes: 0

jq170727
jq170727

Reputation: 14625

Revised Answer

Your original filter was pretty close. Here is one that does what you requested.

(keys_unsorted|first) as $i | with_entries(if .key == $i then .key=$nexID| .value.id=$nexID else . end)

Interactive example within bash: Try it online!

Previous Answer

Rather than use separate invocations you could do the entire operation at once.
For example assuming your complete json is something like

{
  "group": {
    "129": {
      "bounds": { "left": 20, "top": 20, "width": 250, "height": 200 },
      "slot": 88, "userSize": null, "stackTabs": true, "showThumbs": true, "showUrls": true,
      "tileIcons": true, "catchOnce": true, "catchRules": "", "title": "", "id": 129
    }
  },
  "groups_meta": {
    "nextID": 130
  }
}

You could use this filter

   (.group | keys_unsorted | first) as $i
 | (.groups_meta.nextID | tostring) as $n
 | .group |= with_entries( if .key == $i then .key = $n | .value.id = $n else . end ) 

Try it online!

This trusts that the string generated from nextID isn't in group.
If you want to also update nextID you can add something like | .groups_meta.nextID += 1 to the end.

Upvotes: 1

Related Questions