Because of its ubiquity, you're bound to run into a need to manipulate a chunk of JSON in the course of managing your fleet.
For example, you might run a shell script on your Macs that instructs them to
read data from an external system via its API using
curl. That external system
returns a big ol' heap of JSON, but you need to extract only a single data point
from that payload, then act on that value.
This is somewhat of a challenge in the shell because macOS does not include any
native or pre-installed tools to help you hack down that JSON. Administrators
often resort to workarounds like shelling out to Python or using
to approximate parsing.
You may be familiar with using the
osascript binary to execute AppleScript
snippets. While AppleScript is a more common choice for accessing OSA
On the command line, use the
-l (that's a lowercase L) flag with
defaults to expecting AppleScript.
Here's a simple example derived from Apple's sample code:
Using JXA to read JSON in a shell script
Let's look at an example where we use the ip-api.com service to query the current geographic location of the system based on its IP address.
#!/bin/bash GEODATA= curl -s http://ip-api.com/json/ ) read -r -d '' JXA <<EOF
First, we call the ip-api service by using
curl to send a
GET request to the
service's JSON endpoint. The results are stored in a variable called
via command substitution.
Next, we use the
read -r -d '' VARIABLE_NAME <<LIMIT_STRING "bashism" to store
than trying to smoosh it into a single line.
run() is automatically executed when the program is called,
and will print any output returned.
ipinfo references the
GEODATA variable defined in the outer shell script via variable expansion. The
used directly, but for safety we treat it as a string and parse it using the
JSON.parse() method. Since most JSON payloads are multi-line
strings, we wrap the variable expansion in
`` backticks to take
those backticks need to be escaped using
\ backslashes since they're embedded
within a shall script. Whew!
A quick aside on quoting and here-documents: Quoting the limit string (
this case) will prevent variable expansion within the here-document.
<<'EOF' would cause
$GEODATA to be interpretted literally rather than
replaced by the results of the
curl command. The unquoted
<<EOF allows the
variable to expand as expected.
Next, we simply return the
city attribute of the
Finally, we use the
osascript can read input from
stdin, so we use
<<< to read the program
stored in the
JXA variable. Storing its output in the
CITY variable lets us
use it elsewhere in the shell script. In this case we're outputting a nice
message. Running the script will print
This computer is in Atlanta (or
your approximate location) to your terminal.
That was an extremely verbose explanation of a very basic program! But, hopefully it helps explain the patterns being used here. Now let's try something more complex.
Complex JSON manipulation
Here we'll pull the recent commit history of the AutoPkg GitHub repository, then retrieve the commits whose commit message length is longer than the soft 50-character recommended maximum length.
Here's the code:
#!/bin/bash GITHUB= curl -s https://api.github.com/repos/autopkg/autopkg/commits ) read -r -d '' GITCOMMITS <<EOF
The basic structure is largely the same as in the previous example: use
The cool part is being able to manipulate the JSON data in place. Here, we're iterating through each commit and checking the string length of each commit message. If that length is longer than 50 characters, we append that message to an array, along with the commit's SHA hash. Finally, we return a newline-joined string of those commits.
The end result prints only those commit messages longer than 50 characters.
Reading JSON files from disk
You can also read JSON files that exist on disk with just a tiny bit of extra effort.
#!/bin/bash read -r -d '' PARSE <<EOF ; app.includeStandardAdditions = true; EOF
This time, we're using using JXA to get an instance of the "current
application." This lets us do useful things like access the file system. We
define a reusable function to parse a JSON file at a path supplied as a
parameter. Within the
run() function, we call that
parseJSON() function and
supply the full path to a JSON file. We can then access the parsed JSON
properties using standard dot notation.
Other tools and alternatives
Why go through the trouble of using JXA for these use cases?
But why not use another tool or method?
When running code on client systems within a fleet, it's best to use the tools those clients have available by default.
Since Apple will soon stop shipping scripting runtimes such as
Python, Perl, and Ruby, you should not rely on "shelling out" to those
languages. Sure, it might be easy enough to add a
parse=$( python -c 'import json;...' ) call to your script; but at this point you'd just be creating new
technical debt. Don't do that.
If you ship your own Python 3 runtime – such as with "macadmin python" – you could, of course, use that. Installing your own Python 3 framework on your clients is a great idea if you routinely use Python code to manage them. Otherwise, it is another external dependency.
Alternatively, you could pull down jq on your client systems. jq is built specifically to handle JSON on the command line and in shell scripts. But again – it's an external dependency.
Using JXA requires no new installations or configuration changes. It will simply run on your systems as they currently exist.
For that reason, I think JXA a great tool to reach for when you need to work with JSON.
Plus, constraints are fun, right?!
JXA even includes an Objective-C bridge, which allows you to use macOS frameworks like AppKit and Foundation.
The JXA Cookbook on GitHub contains a wealth of information and is an essential read on this topic. It's a great supplement to Apple's somewhat sparse documentation.