0

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?

peak
  • 105,803
  • 17
  • 152
  • 177
Timm
  • 2,488
  • 2
  • 22
  • 25
  • *That may be a problem if there's another key later on with the same name.* An object can **not** have duplicate keys. – oguz ismail Apr 11 '20 at 15:26
  • 1
    Unfortunately ["The JSON Data Interchange Syntax" doesn't say anything about duplicated names (keys).](https://stackoverflow.com/questions/21832701/does-json-syntax-allow-duplicate-keys-in-an-object#23195243) but [**Streaming**](https://stedolan.github.io/jq/manual/#Streaming) can be used to process such json as demonstrated by [these answers](https://stackoverflow.com/search?q=%5Bjq%5D+duplicate+keys+%22--stream%22). – jq170727 Apr 11 '20 at 17:38
  • You're probably right @oguzismail, though I've seen examples on SO with duplicates. But that's not really the point: how to combine the two parts, without having to set intermediate variables and importing them to jq. Can you address that question? – Timm Apr 11 '20 at 19:14
  • @Timm if you include sample inputs and expected output for them, I'll try my best. – oguz ismail Apr 11 '20 at 19:18
  • @oguzismail, I updated the question with the info. If you need more, just comment. Thanks! – Timm Apr 11 '20 at 20:58
  • If either $nextID or $title contain spaces jq --arg nextID $nextID --arg title $title won't expand as you want. In your case $title contains two words (new title) so jq assigns the first word (new) to title then interprets the next word (title) as the filter expression leading to the error you objserved. With bash you almost always want to quote such variables. The [Google Bash Shell Style Guide](https://google.github.io/styleguide/shellguide.html) recommends "${var}" over "$var". The [Unofficial Bash Strict Mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) is also good. – jq170727 Apr 12 '20 at 22:15

2 Answers2

1

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.

jq170727
  • 13,159
  • 3
  • 46
  • 56
  • I updated my question again with a closely related issue – updating values with variables containing spaces – can you check that out? Thanks! – Timm Apr 12 '20 at 12:18
  • I found the trick to use variables with spaces. See my answer. – Timm Apr 12 '20 at 13:55
0

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" } }
Timm
  • 2,488
  • 2
  • 22
  • 25