0

I hope I have represented my problem clearly. Need help querying and them parsing multiple json files using JQ where the structure is non-linear within each file. The application produces config data that can look like this example. There can be zero or many of the DualEndPoint or Local objects per file. I need to be able to query for a specific user in the "User" attribute and insert a new password for resubmission back to the api. For DualEndPoints, the nested object names are variable so one cannot code those values in looking for the "User" attribute.

Where a match for a specific user is found, return the entire structure with only that user's new password inserted. In the example, querying for user1 would return the entire PROFILE1 and PROFILE2, but not PROFILE3 as it doesn't contain user1 credentials.

{
  "PROFILE1": {
    "Type": "ConnectionProfile:FileTransfer:DualEndPoint",
    "WorkloadAutomationUsers": [
      "*"
    ],
    "VerifyBytes": true,
    "TargetAgent": "sqlrptvmjhbpr01",
    "TargetCTM": "Production",
    "Endpoint:Src:Local_0": {
      "Type": "Endpoint:Src:Local",
      "User": "user1",
      "Port": "0",
      "OsType": "Windows",
      "HostName": "Local",
      "Password": "*****",
      "HomeDirectory": "/user1homedir"
    },
    "Endpoint:Dest:SFTP_1": {
      "Type": "Endpoint:Dest:SFTP",
      "User": "user2",
      "HostName": "server2",
      "Password": "*****",
      "HomeDirectory": "/user2homedir"
    }
  },
  "PROFILE2": {
    "Type": "ConnectionProfile:FileTransfer:Local",
    "WorkloadAutomationUsers": [
      "*"
    ],
    "VerifyBytes": true,
    "User": "user1",
    "VerifyDestination": true,
    "OsType": "Windows",
    "HostName": "Local",
    "Password": "*****",
    "TargetAgent": "server1",
    "TargetCTM": "Production"
  },
  "PROFILE3": {
    "Type": "ConnectionProfile:FileTransfer:Local",
    "WorkloadAutomationUsers": [
      "*"
    ],
    "VerifyBytes": true,
    "User": "user3",
    "OsType": "Windows",
    "HostName": "Local",
    "Password": "*****",
    "HomeDirectory": "/user3hoemdir",
    "TargetAgent": "server2",
    "TargetCTM": "Production"
  }
}
BrandonH
  • 31
  • 5
  • Sorry - fixed the formatting in the original post above. – BrandonH Jan 20 '20 at 12:58
  • How does the new password for the user is fed? – Inian Jan 20 '20 at 13:00
  • Normal jq --arg will be added to the command line in the final call: jq --arg PWD "actualpw" '.....' which should replace the specified users' password attribute. The part that's driving me mad is how to address the User attrib at different places in the structure under unknown object names – BrandonH Jan 20 '20 at 13:07
  • Yes coming to that, the `user` attribute is not guaranteed to be within `Endpoint:Dest:` object all the time? – Inian Jan 20 '20 at 13:11
  • It will always be present, but in a different place depending on the value of `.Type`. Where `Type="ConnectionProfile:FileTransfer:Local"` (as in PROFILE3) `User` is guaranteed to be there as a normal object at the top level. Where `Type="ConnectionProfile:FileTransfer:DualEndPoint"`, then there are two more objects that describe source and destination each with their own `User ` objects at the 2nd level. – BrandonH Jan 20 '20 at 13:36
  • @BrandonH, are you limited only to a jq based solution? – Dmitry Jan 21 '20 at 17:24

3 Answers3

2

With jq 1.6 you can use the following :

jq --arg newPwd "newPassword" \
     'walk(if type == "object" and .User == "user1" then .password |= $newPwd else . end)  
        | map_values(select(.. | select(type == "object") and .User == "user1"))' 

This will recurse over your JSON input and set the password field of objects that have a User : "user1" key/value pair to your desired value.

You can try it here.

In anterior versions you can use this equivalent :

jq --arg newPwd "newPassword" \
   'def rec :
      if type == "object" and .User == "user1" then 
        .password = $newPwd
      elif type == "object" then
        map_values(rec)
      elif type == "array" then
        map(rec)
      else
        .
      end
    ; 
    rec  | map_values(select(.. | select(type == "object") and .User == "user1"))'

You can try it here.

Aaron
  • 24,009
  • 2
  • 33
  • 57
  • But do add the way to add `walk()` as a function, in case OP or future readers can't upgrade to a latest version – Inian Jan 20 '20 at 13:46
  • @Inian Do you mean there's a `walk/1` "polyfill" ? Anyway I added a version that can be used in older versions – Aaron Jan 20 '20 at 14:21
  • The walking is great and that answers part of the requirement. The major part of the ask is to filter objects that dont contain any reference to the user1 (which will be an arg in the end) as in PROFILE3 above. Because PROFILE3's object makes no reference to `user1`, the entire structure should be omitted from the output. Thanks for the anterior version as I'm using jq 1.5 – BrandonH Jan 20 '20 at 16:16
  • @BrandonH sorry, I had missed that requirement, I'll update my answer and notify you – Aaron Jan 20 '20 at 16:17
  • @BrandonH sorry about the delay, but if you're still interested I finally came around to updating my answer. It now removes top-level objects which do not contain any reference to a specific user – Aaron Jan 28 '20 at 09:28
  • Thank you - I'll try it out and let you know if it what I need - looks great - learning lots here. – BrandonH Jan 29 '20 at 10:03
1

In the following solution to the stated problem, there are two steps. The first step uses with_entries to select the relevant "PROFILE" objects, and the second step uses walk to update the password provided there is a password. It is easy enough to parameterize everything, so for simplicity let's assume (as in the Q) that the user is "user1":

with_entries(select( .value
    | any(paths(. == "user1");
          .[-1] == "User" )))
| walk( if type == "object" and .User == "user1" and has("Password")
        then .Password = "newpassword"
        else .end)

The use of any here complicates things a bit but is for efficiency.

Note on walk/1

If your jq does not have walk/1, then now would be a good time to update your jq, but if that's not an option, simply google for its def (search terms: jq def walk builtin.jq) and copy the def to the beginning of your jq program.

peak
  • 105,803
  • 17
  • 152
  • 177
0

Welcome to StackOverflow!

The full example was not a valid JSON object, and there is no attempt to solve the problem, or an example of what a desired outcome should look like. So the following answer comes with a bit of guesswork. It would probably increase the odds of getting a good answer greatly if the example was trimmed down to a minimal, reproducible example in which all the clutter is removed.

For example, "TargetAgent": "sqlrptvmjhbpr01" appears to be irrelevant information. The only effect this line has is to increase the cognitive load of the reader in trying to decipher if it's relevant to the problem task, which it appears not to be.

There can be zero or many of the DualEndPoint or Local objects per file.

You don't say exactly what a DualEndPoint or Local object is.

Since the text DualEndPoint only occurs in the context of

"Type": "ConnectionProfile:FileTransfer:DualEndPoint"

I assume that a DualEndPoint object is one that contains a key-value pair with the format

"Type": "...:DualEndPoint"

and that a Local object is the same, but with DualEndPoint substituted with Local. If that interpretation is correct, then there would be three examples of Local objects in your first code snippet at two different levels of nesting (which is what I understand as the "non-linear" part).

One example of a Local object would then be:

{
  "Type": "Endpoint:Src:Local",
  "User": "user1",
  "Port": "0",
  "OsType": "Windows",
  "HostName": "Local",
  "Password": "newpassword",
  "HomeDirectory": "/user1homedir"
}

There aren't any examples of similar objects that, in spite of containing a "User" attribute, should not be updated. So it appears that to answer the question, the distinction between these types of objects is also entirely unnecessary?

I need to be able to query for a specific user in the "User" attribute and insert a new password for resubmission back to the api.

So a sub-problem to your main problem could be to update an object with a new password if it's the right User. Asuming you've scoped the object down to such an object, a sub-part of the program could look like:

$ jq 'if .User == "user1" then .Password = "derp" else . end' local1.json
{
  "Type": "Endpoint:Src:Local",
  "User": "user1",
  "Port": "0",
  "OsType": "Windows",
  "HostName": "Local",
  "Password": "derp",
  "HomeDirectory": "/user1homedir"
}

For DualEndPoints, the nested object names are variable so one cannot code those values in looking for the "User" attribute.

So it sounds like you want to arbitrarily recurse looking for objects with "User" attributes. Some of jq's recursion combinators are .., the more general recurse and what appears to be more appropriate in this context, walk:

$ jq 'walk(if type == "object" and .User == "user1"
           then .Password = "derp"
           else . end)' full.json

(This is also what Aaron posted, except he uses |= and I use =.)

See his jqplay example or this jqplay example.

Where a match for a specific user is found, return the entire structure with only that user's new password inserted. In the example, querying for user1 would return the entire PROFILE1 and PROFILE2, but not PROFILE3 as it doesn't contain user1 credentials.

This sounds like an extra condition in the expression we walk with:

$ jq 'walk(if type == "object" and has("User")
           then (if .User == "user1"
                 then .Password = "derp"
                 else null end)
           else . end)' full.json

This appears to almost work (see this jqplay example), except it leaves "foo": null values as the result of having walked. This is a by-product of already having recursed down into the object containing the "User" property, which makes it hard to express that the parent key-value pair should be deleted.

Fixing this we need to either look ahead in walk/1's filter or create a placeholder and walk a second time with the perspective of the parent object. The latter of these two strategies is demonstrated here:

$ jq 'walk(if type == "object" and has("User")
      then (if .User == "user1" then .Password = "derp" else "wat" end)
      else . end)
      | walk(if type == "object"
             then with_entries(select(.value != "wat"))
             else . end)' full.json

This appears to work. See this jqplay example.

sshine
  • 15,635
  • 1
  • 41
  • 66
  • Thankx - This is great progress. Two issues: 1) Where the Type is a DualEndpoint, both parts of the sub-objects must still appear - ie: the Src and the Dest 2) Could you possibly reformat for jq version 1.5 which doesn't have the walk predicate. – BrandonH Jan 20 '20 at 16:19
  • @BrandonH: I'm not sure I feel like doing that. :-) Could you possibly change your expectations? `walk/1` was written in jq, so you can port it from 1.6 by prepending [this `def walk(f): ...;`](https://github.com/stedolan/jq/blob/master/src/builtin.jq#L255). – sshine Jan 21 '20 at 08:25
  • Thank you - you have been extremely helpful. – BrandonH Jan 21 '20 at 09:14