Check and Change JSON Values with jq

jqjsontext processing

[
    {
        "name": "user1",
        "status": "off"
    },
    {
        "name": "user2",
        "status": "off"
    },
    {
        "name": "user3",
        "status": "on"
    }
]

I want to know how I can search a JSON file recursively for the value of status, for example, off, and get all the name values for which the status is off.

Also, how can I change the value for user1 and user2 to on?

I am using jq and bash.

Best Answer

To apply an expression to all elements of an array, and get an altered array out, use map(expression).

Extracting all entries that have off status:

$ jq 'map(select(.status == "off"))' file
[
  {
    "name": "user1",
    "status": "off"
  },
  {
    "name": "user2",
    "status": "off"
  }
]

Mapping select(.status == "off") will alter the array by extracting (selecting) only the elements for which .status == "off" is true.

Extracting only the decoded names from the above:

$ jq -r 'map(select(.status == "off"))[].name' file
user1
user2

Tucking on [].name at the end of the previous expression will first expand the array into a set of individual entries (that's what [] does) and then extract the .name value from each of those.

You could also have used

jq -r 'map(select(.status == "off").name)[]' file

... which creates an array of names from which you then extract all elements.

Setting the status to on for all entries that have an off status:

$ jq 'map(select(.status == "off").status = "on")' file
[
  {
    "name": "user1",
    "status": "on"
  },
  {
    "name": "user2",
    "status": "on"
  },
  {
    "name": "user3",
    "status": "on"
  }
]

By mapping select(.status == "off").status = "on", we modify our array by setting the status to on for any element whose status is off.

Setting the status to on for entries based on explicit names:

$ jq 'map(select(.name == "user1" or .name == "user2").status = "on")' file
[
  {
    "name": "user1",
    "status": "on"
  },
  {
    "name": "user2",
    "status": "on"
  },
  {
    "name": "user3",
    "status": "on"
  }
]

The same thing, but don't hard-code the names inside the expression. Rather, give them as a list at the end of the command line (note that --args and the list of names must be the last thing on the command line):

$ jq 'map(select(IN(.name; $ARGS.positional[])).status = "on")' file --args user1 user2
[
  {
    "name": "user1",
    "status": "on"
  },
  {
    "name": "user2",
    "status": "on"
  },
  {
    "name": "user3",
    "status": "on"
  }
]

The list of names given to --args is found in the array called $ARGS.positional.

The IN(a; b) thing returns true if a appears in the set b. We're using $ARGS.positional[] as the set to search, which is the set of the names given on the command line. This means our select() here will extract only the elements whose names occur in the list on the command line, and these will have their status set to on.

Note that IN(a; b) differs from the similarly named in(a), which returns true if a given value occurs as a key or index in the object or array a.

Setting all entries' status to on:

$ jq 'map(.status = "on")' file
[
  {
    "name": "user1",
    "status": "on"
  },
  {
    "name": "user2",
    "status": "on"
  },
  {
    "name": "user3",
    "status": "on"
  }
]

Follow-up question in comments:

And If I want to match like NOT IN(a, b), then? Clearly, I am asking if I pass value like user1 and it turns off all other than user1. How can I do so?

The following uses an explicit if statement to change the status to on for the given names and to off for all others:

jq '
    map(
        if IN(.name; $ARGS.positional[]) then
            .status = "on"
        else
            .status = "off"
        end
    )' file --args user1

Or, in a one-line layout:

jq 'map(if IN(.name; $ARGS.positional[]) then .status = "on" else .status = "off" end)' file --args user1

If I instead interpret the follow-up as not wanting to change the status of the given names, no matter what their statuses are, but to change all other statuses to off. In that case, we can negate the condition used in a previous variation:

jq 'map(select(IN(.name; $ARGS.positional[]) | not).status = "off")' file --args user1

Note the added | not after IN(...; ...).

Of course, you could have used an if statement in most of the above variations too, but I like using select() when I'm able to.

Related Question