Plutil JSON parsing for Fun and Profit

The latest in the Fun and Profit series!

So in my last post we talked about the new Jamf Pro API and we also talked a little bit about using JavaScript through the osascript binary to parse the results. Rich Trouton pointed out in a blog post today as well as on the Mac Admins Slack that there is another way.

However I need to point out that my testing shows that it is limited to macOS 12 Monterey on onward.

As long as you’re ok with that limitation, let’s proceed.

Let’s start with an example piece of json output from Jamf Pro, suitably sanitised. Let’s also assume this output exists in a variable called “jsonoutput”.

{
  "totalCount" : 1,
  "results" : [ {
    "id" : "1234",
    "udid" : "ABCDEF12-3456-789A-BCDE-F123456789AB",
    "general" : null,
    "diskEncryption" : null,
    "localUserAccounts" : null,
    "purchasing" : null,
    "printers" : null,
    "storage" : null,
    "applications" : null,
    "userAndLocation" : null,
    "configurationProfiles" : null,
    "services" : null,
    "plugins" : null,
    "hardware" : null,
    "certificates" : null,
    "attachments" : null,
    "packageReceipts" : null,
    "fonts" : null,
    "security" : null,
    "operatingSystem" : null,
    "licensedSoftware" : null,
    "softwareUpdates" : null,
    "groupMemberships" : null,
    "extensionAttributes" : [ {
      "definitionId" : "12",
      "name" : "EA Dropdown Test 1",
      "description" : "Dropdown test for JSON parsing 1",
      "values" : [ ],
      "dataType" : "STRING",
      "options" : [ "Enabled" ],
      "inputType" : "POPUP",
      "enabled" : true,
      "multiValue" : false
    }, {
      "definitionId" : "24",
      "name" : "EA Dropdown Test 2",
      "description" : "Dropdown test for JSON parsing 1",
      "values" : [ "Enabled" ],
      "dataType" : "STRING",
      "options" : [ "Enabled" ],
      "inputType" : "POPUP",
      "enabled" : true,
      "multiValue" : false
    } ],
    "contentCaching" : null,
    "ibeacons" : null
  } ]
}

Let’s start with something simple. We just want to read out the totalCount at the top of the output. We’d do that easily with this:

rp@MyMac ~ % /usr/bin/plutil -extract totalCount raw -o - - <<< "$jsonoutput"
1
rp@MyMac ~ % 

To explain what’s going on here let’s step through the command. The -extract tells plutil to look for an entry to display at a specific “keypath”. The totalCount is the keypath to search. “raw” is the data format and the -o – – tells plutil to accept put from a stdin variable rather than a file and then output to stdout. Finally the triple less than is the shell redirect for the variable.

Now something more complex. We want to read out the id field as that’s the Jamf Pro ID for the Mac we’re on. With plutil we have to get a little creative and the command looks like this:

rp@MyMac ~ % /usr/bin/plutil -extract "results".0."id" raw -o - - <<< "$jsonoutput"
1234
rp@MyMac ~ % 

Now let’s explain the keypath formatting because this confused the heck out of me for some time.

We want the id field. The id field sits below results so you’d think oh let’s just do “results”.”id” … well not really. Results field starts with a square bracket and in JSON that means a list or array. Data surrounded by curly brackets is a key / value pair and you can have a list of data fields.

Since results is an array we have to specify an index number to search. Thankfully in this example there’s only one of them so you can specify a zero. That’s why the command example above is “results”.0.”id” … we’re asking plutil to search the first array entry only.

Finally something far more complex. We want to read out one of the extension attributes to see what value is set. Here’s the issues that await us. We can’t scan all the EA entries at once, and when we scan an EA we can only scan for one entry in that EA at a time. It requires slightly different thinking to how you normally batch process data in shell languages.

The solution? Loops. It turns out we can do a query with plutil and get the number of entries in the extension attributes section and that looks like this:

rp@MyMac ~ % index=$( /usr/bin/plutil -extract results.0.extensionAttributes raw -o - - <<< "$jsonoutput" )
rp@MyMac ~ % echo $index
2
rp@MyMac ~ % 

Excellent. Now we know we’re starting at 1 and we’ve a maximum of 2 to go through. Let’s make a loop in zsh using that information.

for i in {0.."$index"}
do
   # Do things here
done

We’ve got our number of indexes, and we’ve got our loop. How do we find the information we want? Iterate through all the data, and we can do this because we did the difficult bit of reading it into a variable earlier so this shouldn’t be computationally expensive. My current solution looks like this:

for i in {0.."$index"}
do
        # Find the name of the current EA we're processing
	name=$( /usr/bin/plutil -extract results.0.extensionAttributes.$i.name raw -o - - <<< "$eainfo" )

        # Does the name match what we're looking for
        # If so, find it's value
	[ "$name" = "EA Dropdown Test 2" ] && setting=$( /usr/bin/plutil -extract "results".0."extensionAttributes".$i."values".0 raw -o - - <<< "$eainfo" 2>/dev/null )
done

That will either return a null if the option is empty or a value if it is set to something. You can then test to see if it is the value you require.

It’s important to again stress that the code above is only good for macOS 12 Monterey. We are making extensive use of the “raw” data feature and plutil has the following entry at the bottom of its manual page.

STANDARDS
     The plutil command obeys no one's rules but its own.

HISTORY
     The plutil command first appeared in macOS 10.2.

     The raw format type, -type command, -expect option, and -append option
     first appeared in macOS 12.