Determining “iCloud Private Relay” and “Limit IP tracking” status in macOS

If you are a Mac admin you might have noticed some Apple plists are more complex than others these days. As evidenced in my post Don’t Be a Jerk, getting the status of Do Not Disturb in Big Sur was a multi-step exercise. Check out this modified code excerpt from doNotDisturb‘s Big Sur handling

#!/bin/sh

#Big Sur DnD status, returns "true" or [blank] (to be run as console user)
dndStatus="$(/usr/libexec/PlistBuddy -c "print :userPref:enabled" /dev/stdin 2>/dev/null <<< "$(plutil -extract dnd_prefs xml1 -o - /dev/stdin <<< "$(defaults export com.apple.ncprefs.plist -)" | xmllint --xpath "string(//data)" - | base64 --decode | plutil -convert xml1 - -o -)")"

#if we have ANYTHING it is ON (return 0) otherwise fail (return 1)
[ -n "${dndStatus}" ] && { echo ON; exit 0; } || { echo OFF; exit 1; }

Why is it so complex? Well because with the user preference domain of com.apple.ncprefs there is a base64 encoded plist embedded in the dnd_prefs key and requires some massaging to get it to a state where PlistBuddy can read and then extract the status from the :userPref:enabled key. See it’s just that easy! 😅

In macOS Monterey and higher Apple is now using JSON for Focus status which is great because it can can be parsed much easier (perhaps using my ljt or jpt tools) but sometimes not even that is needed! Sometimes file presence or awking is sufficient to get a reliable result too. See the evolution of doNotDisturb.sh for macOS for the myriad techniques one can use.

Now, here we are at Monterey on the verge of Ventura and not all areas of macOS have seen the JSON light and are still using nested base64-encoded Plist blobs to store the states of new features like iCloud Private Relay but luckily not the per interface setting of Limit IP Tracking.

iCloud Private Relay Status

Now why would you want to know these statuses? Perhaps you have a VPN product that doesn’t work when Private Relay is turned on or if Limit IP Tracking is turned on for a particular interface and you need to alert the user. Additionally maybe there’s an issue if your VPN browser handshake fails when Safari is used, if so see: Determining URL scheme handlers in macOS. Back to the task at hand though let’s turn some prefs inside out and look at iCloud Relay status with iCloudPrivateRelayStatus.sh

#!/bin/bash
: <<-LICENSE_BLOCK
iCloud Private Relay Status Checker (20250204) - (https://github.com/brunerd)
Copyright (c) 2025 Joel Bruner (https://github.com/brunerd)
Licensed under the MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LICENSE_BLOCK

#############
# FUNCTIONS #
#############

#include this self-contained function in your script
function iCloudPrivateRelay()(

	#only for Moneterey and up, 11 and under need not apply
	[ "$(sw_vers -productVersion | cut -d. -f1)" -le 11 ] && return 1
	
	#parent pref domain
	domain="com.apple.networkserviceproxy"
	#key that contains base64 encoded Plist within parent domain
	key="NSPServiceStatusManagerInfo"
	#path within base64 embedded plist, there are multiple PrivacyProxyServiceStatus entries but this one seems primary
	targetPath=':$objects:1:PrivacyProxyServiceStatus'

	#get the top level data from the main domain
	parentData=$(launchctl asuser "$(stat -f %u /dev/console)" sudo -u "$(stat -f %Su /dev/console)" defaults export "${domain}" -)

	#if domain does not exist, fail
	[ -z "${parentData}" ] && return 1

	#export and decode the base64 data as plist XML and look for 
	keyStatusCF=$(/usr/libexec/PlistBuddy -c "print '${targetPath}'" /dev/stdin 2>/dev/null <<< "$(plutil -extract "${key}" xml1 -o - /dev/stdin <<< "${parentData}" | xmllint --xpath "string(//data)" - | base64 --decode | plutil -convert xml1 - -o -)")
	#if no value, fail
	[ -z "${keyStatusCF}" ] && return 1
	
	#if true/1 it is on, 0/off (value is integer not boolean BTW)
	[ "${keyStatusCF}" = "1" ] && return 0 || return 1
)

########
# MAIN #
########

#example - multi-line if/else calling
if iCloudPrivateRelay; then
	echo "iCloud Private Relay is: ON"
else
	echo "iCloud Private Relay is: OFF"
fi

Even though we work really hard to get the plist data extracted we don’t need to walk the entire XML document (you will find PlistBuddy balks at exporting JSON for a number of reasons). Instead we look for the presence of the key name PrivacyProxyServiceStatus and that is sufficient to reliably detect the state. UPDATE: This ambiguity has been resolved and it will now look at the path :$objects:1:PrivacyProxyServiceStatus for the status, which seems to be consistent (LMK otherwise). I have 2 versions in my GitHub iCloudPrivateRelayStatus.sh and the minified one line version iCloudPrivateRelayStatus.min.sh

Limit IP Tracking Status

Limit IP Tracking, is a per-interface setting that is on by default, however has no effect unless Private Relay is enabled. For this we will use ljt my Little JSON Tool to retrieve the value from the massaged JSON conversion of com.apple.wifi.known-networks.plist if on WiFi or if on Ethernet .../SystemConfiguration/preferences.plist. (Update: Now macOS Sequoia compatible after Apple broke the usual way of getting the SSID name for WiFi connections)

#!/bin/bash
: <<-LICENSE_BLOCK
Limit IP Tracking Status Checker (20250204) - (https://github.com/brunerd)
Copyright (c) 2025 Joel Bruner (https://github.com/brunerd)
Licensed under the MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
LICENSE_BLOCK

#############
# FUNCTIONS #
#############

#include this self-contained function in your script
function limitIPTracking()(

	#only for Moneterey and up, 11 and under need not apply
	[ "$(sw_vers -productVersion | cut -d. -f1)" -le 11 ] && return 1

	#Little JSON Tool (ljt) - https://github.com/brunerd/ljt - MIT License
	function ljt () ( #v1.0.8 ljt [query] [file]
	{ set +x; } &> /dev/null; read -r -d '' JSCode <<-'EOT'
	try{var query=decodeURIComponent(escape(arguments[0]));var file=decodeURIComponent(escape(arguments[1]));if(query===".")query="";else if(query[0]==="."&&query[1]==="[")query="$"+query.slice(1);if(query[0]==="/"||query===""){if(/~[^0-1]/g.test(query+" "))throw new SyntaxError("JSON Pointer allows ~0 and ~1 only: "+query);query=query.split("/").slice(1).map(function(f){return"["+JSON.stringify(f.replace(/~1/g,"/").replace(/~0/g,"~"))+"]"}).join("")}else if(query[0]==="$"||query[0]==="."&&query[1]!=="."||query[0]==="["){if(/[^A-Za-z_$\d\.\[\]'"]/.test(query.split("").reverse().join("").replace(/(["'])(.*?)\1(?!\\)/g,"")))throw new Error("Invalid path: "+query);}else query=query.replace("\\.","\udead").split(".").map(function(f){return"["+JSON.stringify(f.replace("\udead","."))+"]"}).join("");if(query[0]==="$")query=query.slice(1);var data=JSON.parse(readFile(file));try{var result=eval("(data)"+query)}catch(e){}}catch(e){printErr(e);quit()}if(result!==undefined)result!==null&&result.constructor===String?print(result):print(JSON.stringify(result,null,2));else printErr("Path not found.")
	EOT
	queryArg="${1}"; fileArg="${2}";jsc=$(find "/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/" -name 'jsc');[ -z "${jsc}" ] && jsc=$(which jsc);[ -f "${queryArg}" -a -z "${fileArg}" ] && fileArg="${queryArg}" && unset queryArg;if [ -f "${fileArg:=/dev/stdin}" ]; then { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "${fileArg}"; } 1>&3 ; } 2>&1); } 3>&1;else [ -t '0' ] && echo -e "ljt (v1.0.8) - Little JSON Tool (https://github.com/brunerd/ljt)\nUsage: ljt [query] [filepath]\n  [query] is optional and can be JSON Pointer, canonical JSONPath (with or without leading $), or plutil-style keypath\n  [filepath] is optional, input can also be via file redirection, piped input, here doc, or here strings" >/dev/stderr && exit 0; { errOut=$( { { "${jsc}" -e "${JSCode}" -- "${queryArg}" "/dev/stdin" <<< "$(cat)"; } 1>&3 ; } 2>&1); } 3>&1; fi;if [ -n "${errOut}" ]; then /bin/echo "$errOut" >&2; return 1; fi
	)
	
	#sequoia broke the old ways of getting SSID
	function getCurrentWiFiSSIDforInterface(){
		#no interface, error
		[ -z "${1}" ] && return 1

		#get the SSID _quickly_ thanks MacAdmins @jby
		local currrent_SSID=$(ipconfig getsummary "${1}" | awk -F ' SSID : ' '/ SSID : / {print $2}')
		
		[ -z "${currrent_SSID}" ] && return 1
		
		echo "${currrent_SSID}"; return 0
	}

	#test interface specified or fallback to default interface
	interfaceID=${1:-$(route get 0.0.0.0 2>/dev/null | awk '/interface: / {print $2}')}

	#WIFI: key: PrivacyProxyEnabled, file: /Library/Preferences/com.apple.wifi.known-networks.plist
	#if no error getting WiFi SSID, then we are WiFi
	if wifiSSID=$(getCurrentWiFiSSIDforInterface "${interfaceID}"); then

		#key name inside plist
		keyName="wifi.network.ssid.${wifiSSID}"

		#oddly this file is only read-able by root
		if [ ! -r /Library/Preferences/com.apple.wifi.known-networks.plist ]; then
			echo "Insufficient preferences to determine WiFi state, run as root" >/dev/stderr
			exit 1
		fi

		#get JSON version, so much easier to get around, convert date and data types to strings
		wifiKnownNetworks_JSON=$(defaults export /Library/Preferences/com.apple.wifi.known-networks.plist - | sed -e 's,<date>,<string>,g' -e 's,</date>,</string>,g' -e 's,<data>,<string>,g' -e 's,</data>,</string>,g' | plutil -convert json -o - -)
		
		#if there is NO entry, then it is active, it is opt-out designed
		PrivacyProxyEnabled=$(ljt "/wifi.network.ssid.${wifiSSID}/PrivacyProxyEnabled" 2>/dev/null <<< "${wifiKnownNetworks_JSON}")
		
		if [ "${PrivacyProxyEnabled}" = "false" ]; then
			return 1
		fi
	#ETHERNET: key: DisablePrivateRelay, file: /Library/Preferences/SystemConfiguration/preferences.plist, 
	else
		#get JSON, easily converts with not data or date types within
		systemConfigPrefsJSON=$(plutil -convert json -o - /Library/Preferences/SystemConfiguration/preferences.plist)
		#get current set UUID
		currentSet=$(echo "${systemConfigPrefsJSON}" | ljt /CurrentSet 2>/dev/null)
		#get value for current default interface of current Location set
		DisablePrivateRelay=$(ljt "${currentSet}"/Network/Interface/${interfaceID}/DisablePrivateRelay  2>/dev/null <<< "${systemConfigPrefsJSON}")

		#if it is TRUE we are Disabled, then we are NOT ON, return fail code
		if [ "${DisablePrivateRelay}" = "1" ]; then
			return 1
		fi
	fi
)

########
# MAIN #
########

if [ ! -r /Library/Preferences/com.apple.wifi.known-networks.plist ]; then
	echo "Insufficient privileges to determine WiFi state, run as root" >/dev/stderr
	exit 1
fi

#use default interface if nothing is specified for argument $1
interface=${1:-$(route get 0.0.0.0 2>/dev/null | awk '/interface: / {print $2}')}

#returns 0 if ON and 1 if OFF, you may supply an interface or it will fall back to the default
limitIPTracking "${interface}" && echo "Limit IP tracking is ON for: ${interface}" || echo "Limit IP tracking is OFF for: ${interface}"

You can specify an interface or it will use the default interface (there’s a good one-liner to use route get 0.0.0.0 to figure that out). plutil is not initially used to convert com.apple.wifi.known-networks.plist to JSON as it will fail due to XML/plist data types like <date> and <data> that do not have JSON equivalents. First we use sed to change them to <string> types then plutil can convert to JSON. After that it’s a cake walk for ljt to get the value and report back. If the interface is not WiFi then /Library/Preferences/SystemConfiguration/preferences.plist has none of those conversion issues. Check out limitIPTrackingStatus.sh and the minified limitIPTrackingStatus.min.sh at my GitHub.

Thanks for reading! P.S. I won’t be at JNUC 2022, better luck next year!

Decoding macOS automatic login details

In my previous post, Automating automatic login, we looked at how to create the /etc/kcpassword file used for automatic login by using only shell script and built-in command line tools. Why shell only? In preparation for the great scripting runtime deprecation yet to come, I say! Now it’s time to do the reverse for auto login. Let’s get those details back out! Who would need to do such a thing? Imagine a scenario where you the hapless Mac admin have inherited a bunch of Zoom Room Mac minis with auto-login enabled yet no one has documented the passwords used for them! If they are enrolled in Jamf there’s no need to guess what annoying l33t sp3@k password was used, let’s leverage our XOR’ing skills and knowledge of how kcpassword works to send those details back to Jamf.

To get the password back out of /etc/kcpassword we XOR the password again with the same cipher used to obfuscate it originally however but instead of padding it in multiples of 12, we will stop when a character is the same as the current cipher character. FYI when you XOR a value with itself the result is 00 but that’s an unnecessary operation, we can just compare the characters. Voilá, that’s it.

Here’s the gist of the kcpasswordDecode routine:

Now for something a bit more useful to those with Jamf or other management tools: getAutoLogin. It reports the auto login username, if set, and the decodes the /etc/kcpassword file, if present. Note that until macOS 12 Monterey /etc/kcpassword was not removed when Automatic Login was turned off in System Preferences! Here’s what getAutoLogin looks in the Jamf policy logs:

Plaintext passwords in your logs are probably not the best, but hey, how else you gonna figure out your dang Zoom Room passwords? After retrieving the credentials and storing somewhere more secure, like a password manager, make sure to Flush the policy logs! Thanks for reading, I hope this comes in handy or at the very least was informative and mildly entertaining. 🤓

Gist: kcpasswordDecode
Github: getAutoLogin

Automating automatic login for macOS

I recently had some Zoom Room Macs that needed some automation love and I thought I’d share how you can enable Automatic Login via script, perhaps you have several dozen and use Jamf or some other Mac management tool? Interested? Read on! Currently what’s out there are either standalone encoders only or part of larger packaging workflow to create a new user. The standalone encoders lacked some niceties like logging or account password verification and the package method added the required dependency of packaging if any changes were required. Above all, every script required Python, perl or Ruby, which are all on Apple’s hit list for removal in an upcoming OS release. For now macOS Monterey still has all of these runtimes but there will come a day when macOS won’t and will you really want to add 3rd party scripting runtimes and weaken your security by increasing attack surface when you can weaken your security using just shell? 😜 So for some fun, I re-implemented the /etc/kcpassword encoder in shell so it requires only awk, sed, and xxd, all out of the box installs. I also added some bells and whistles too.

Some of the features are:

  • If the username is empty, it will fully disable Automatic Login. Since turning it off via the System Preferences GUI does not remove the /etc/kcpassword file if it has been enabled (!)
  • Ensures the specified user exists
  • Verifies the password is valid for the specified user
  • Can handle blank passwords
  • Works on OS X 10.5+ including macOS 12 Monterey

For the Jamf admin the script is setAutomaticLogin.jamf.sh and for standalone usage get setAutomaticLogin.sh, both take a username and password, in that order and then enable Automatic Login if it all checks out. The difference with the Jamf script is that the first parameter is ${3} versus ${1} for the standalone version.

Also here’s a well commented Gist as a little show and tell for what the shell only version of the kcpassword encoder looks like. Enjoy!

Update: I meant to expound on how it didn’t seem that padding kcpassword to a length multiple of 12 was necessary, since it had been working fine on the versions I was testing with but then I tested on Catalina (thinking of this thread) and was proven wrong by Catalina. I then padded with 0x00 with HexFiend in a successful test but was reminded that bash can’t handle that character in strings, instead I padded with 0x01, which worked on some macOS versions but not others. Finally, I settled on doing on what macOS does somewhat, whereas it pads with the next cipher character then seemingly random data, I pad with the cipher characters for the length of the padding. This works for Catalina and other of macOS versions. 😅

Big Hat Tip to Pico over at MacAdmins Slack for pointing out this issue in pycreateuserpkg where it’s made clear that passwords with a lengths of 12 or multiples thereof, need another 12 characters of padding (or in some newer OSes at least one cipher character terminating the kcpassword data, thanks! 👍

secret origins: the jpt

On building a JSON tool for macOS without using Python, perl, or Ruby.

In my work as a Macintosh engineer and administrator I’ve noticed macOS has lacked a bundled tool for working with JSON at the command line. Where XML has its xpath, if your shell script needs some JSON chops, it’ll require an external binary like jq or something else scripted in Python, Ruby or perl using their JSON modules. The problem is, those runtimes have been slated for removal from a future macOS. So I took that as challenge to devise a method to query and modify JSON data within shell scripts, that didn’t use one of those deprecated scripting runtimes and didn’t require an external binary dependency either. Could I achieve robust and native JSON parsing on a Mac by simply “living off the land”?

Why not just re-install the runtimes when Apple deprecates them?

“Why limit yourself like this? Just re-install the runtimes and move on”, you may ask. Well, I’d like to think that limitations can inspire creativity but we should also consider there may be some other reasons why Apple is discontinuing the inclusion of those runtimes. Some may say, “Apple Silicon + macOS 11.0 is the perfect time for them to clean house”, to which I’d have to agree that’s a very good reason and likely a factor. Others could say they are looking to tighten the screws to keep out unsigned code: maybe, they do like to glue things shut! But really, I think it’s more akin to the web-plugins of yore like Java and Flash. Apple does not want to be the conduit for deploying 3rd party party runtimes which increase the attack surface of macOS. This seems like the most reasonable of the explanations. So, if you accept that Apple is attempting to reduce attack surface, why increase it by re-installing Python, Ruby, or perl, just so a transient script (like a Jamf Extension Attribute) can parse a JSON file? My answer to that, is you don’t. You play the hand you’re dealt. Game on!

Looking for truffles (in a very small back yard)

Despite having another project (shui) that can output and invoke Applescript from within a shell scripts for generating user interfaces, I definitely knew that Applescript was not the way to go. Apple however, added to the languages Open Scripting Architecture (OSA) supports back in 2014 with OS X Yosemite (10.10), they added Javascript along with a bridge to the Cocoa Objective-C classes and they called it JXA: Javascript for Automation. This seemed like a promising place, so I started playing with osascript and figured out how to load files and read /dev/stdin using JXA, and while looking for an answer for garbled input from stdin I came upon a Japanese blog that mentioned jsc the JavaScriptCore binary which resides in the /System/Library/Frameworks/ JavaScriptCore.framework. Arigato! Pay dirt! 🤑 jsc does exactly what we need it to do: It can interpret Javascript passed as an argument, can access the filesystem and read from /dev/stdin, and best of all is in non-Private System level Framework that exists all the way back to OS X 10.4! Just the kind of foundation on which to build the tool.

Homesteading jsc

The existence of jsc goes back all the way to OS X Tiger and it’s functionality has evolved over the years. In order to have a consistent experience in jpt from macOS 10.4 – 11.x+ a few polyfills had to be employed for missing functions, along with a few other workarounds regarding file loading, printing and exit codes (or lack thereof). Once those were addressed the jsc proved to be a highly optimized Javascript environment that’s blazingly fast. It spans 13 macOS releases and is even present in many Linux distros out-of-the-box (Ubuntu and CentOS) and can even be run on Windows when the Linux Subsytem is installed.

With the host environment sorted, I began working with the original JSONPath code by Stefan Goessner as the query language. I didn’t know about JSON Pointer yet so this strange beast was all I knew! It worked out really. I went full throttle into developing the “swiss army knife” of JSON tools. I really leaned into the Second System Effect, as described in the Mythical Man-month, it’s when you put every doodad, gizmo and doo-hicky in your 2nd product (my first simple JSON pretty-printer built in JS on jsc). Eventually though, after I reached a feature plateau, I came back around to the address the quirks of the original JSONPath code. I ended up rewriting signifigant chunks of JSONPath and released it as it’s own project: brunerd JSONPath. But I digress, let’s get back to the JSON Power Tool.

jpt: powers and abilities

At it’s most basic, jpt will format or “pretty print” any JSON given. jpt can also handle data retrieval from JSON document using either JSONPath or JSON Pointer syntax. JSONPath, while not a standard, is a highly expressive query language akin to XPath for XML, with poweful features like recursive search, filter expressions, slices, and unions. JSON Pointer on the other hand is narrower in focus, succint and easily expressed, and standardized but it does not offer any of the interogative features that I feel make JSONPath so intriguing. Finally, jpt can also modify values in a JSON document using standardized JSON Patch operations like: add, remove, replace, copy, move, and test, as well as the also standardized JSON Merge Patch operation. Altogether, the jpt can format, retrieve and alter JSON documents using only a bit of outer shell script plus a lot more Javascript on any macOS since 10.4! 😅

Where can I get the jpt?

Stop by the project’s GitHub page at: https://github.com/brunerd/jpt

There you will find the full source and also a minified version of the jpt for inclusion within your shell scripts. Since it’s never compiled you can always peer inside and learn from it, customize it, modify it, or just tinker around with it (usually the best teacher).

Future Plans

There will undoubtedly be continued work on the jpt. Surely there are less than optimal routines, un-idiomatic idioms, edge cases not found, and features yet to be realized. But as far as the core functionality goes though, it’s fairly feature complete in my opinion. Considering that one of my top 10 StrengthsFinder qualities is “Maximizer”, the odds are pretty good, I’ll keep honing the jpt‘s utility, size (smaller), and sharing more articles with examples on the kinds of queries and data alteration operations the jpt can so perform. Stay tuned!

New Projects: jpt and shui, Now Available

Between March and October 2020 I had some great ideas for command line Mac utilities the MacAdmin could apprecite and I had the time to devote to their realization. I’m excited to present these two open source projects, available on GitHub: jpt and shui. I hope they can add richness to your shell scripts’ presentation and capabilities without requiring additional external dependancies.

jpt – the “JSON Power Tool” is a Javascript and shell script polyglot that leverages jsc, the JavascriptCore binary that is standard on every Mac since 10.4 and since the jpt is purposefully written in ES5 to maintain maximum compatibility, why yes, this tool does run on both PPC and Intel Macs all the way back to OS X Tiger and then all the way forward to the latest 11.0 macOS Big Sur! Many Linux distros like CentOS and Ubuntu come with jsc pre-installed also, even Windows with the Linux Subsystem installed can run jsc and therefore can run the jpt!

What you can do with the jpt? Query JSON documents using either the simple yet expressive JSONPath syntax or the singular and precise JSON Pointer (RFC6901) syntax. The output mode is JSON but additional creative output modes can render JSONPaths, JSON Pointer paths, or even just the property names with their “constructor” types (try -KC with -J or -R) Textual output can be encoded in a variety of formats (hex/octal/URI encoding, Unicode code points, etc…), data can be modified using both JSON Patch (RFC6902) operations (add, replace, remove, copy, move, test) and also JSON Merge Patch (RFC7386) operations. JSON can be worked with in new ways, try -L for “JSONPath Object Literal” output to see what I mean. Or you simply feed jsc a file to pretty-print (stringify) to /dev/stdout. I’ll be writing more about this one for sure.
Github project page: jpt
Tagged blog posts: scripting/jpt

shui – first-class Applescript dialog boxes in your shell scripts without needing to remember esoteric Applescript phrasings! If you think it’s odd for code to have possessive nouns and are more comfortable in shell, you’re not alone. shui can be embedded in either bash or zsh scripts but it can also output Applescript if you really want to know how the sausage is made or want to embed in your script without shui. Hopefully shui will let you forget those awkward Applescript phrasing and focus on your shell script’s features and functionality. It uses osascript to execute the Applescript and launchctl to invoke osascript in the correct user context so user keyboard layouts are respected (vs. root runs). Check out the project page for demo videos and then give shui a try.
Project page: shui
Tagged blog posts: scripting/shui

macOS shell games: long live bash

TL;DR – Bash ain’t goin’ nowhere on Mac, both version-wise and in terms of its presence. Looking at the longevity of other shells on the system, it will likely be around for a good while longer.

There’s been a lot of hand wringing and angst online about bash and zsh becoming the new default shell. Some folks feel Apple is signaling deprecation and removal and have the crushing feeling they must convert all their bash script to zsh. I think that’s a bit unnecessary.

True, the default shell is changing from bash to zsh, as Apple notes here. This is indeed a Good Thing™ as zsh shell has been one of the most frequently updated shells on macOS. Bash, on the other hand, has been stuck at varying versions of 3.2 for 12 years now! On the plus side, sysadmins have “enjoyed” predictable and stable behavior from bash during this time. Sure, you’d love new features but when you are scripting for the enterprise, across multiple OS versions, this is just the sort of thing you want: boringness and dependability.

As far as deprecations go, the only thing Apple has signaled as being deprecated eventually are scripting language runtimes (not shells):

Scripting language runtimes such as Python, Ruby, and Perl are included in macOS for compatibility with legacy software. Future versions of macOS won’t include scripting language runtimes by default, and might require you to install additional packages. If your software depends on scripting languages, it’s recommended that you bundle the runtime within the app. (49764202)

Use of Python 2.7 isn’t recommended as this version is included in macOS for compatibility with legacy software. Future versions of macOS won’t include Python 2.7. Instead, it’s recommended that you run python3 from within Terminal. (51097165)

mac OS Catalina 10.15 Release Notes

I’ve done some digging and culled the shell versions from OS X 10.0 to macOS 10.15, along with their respective release dates. I think it shows that shells, no matter how old and crusty, tend to be long lived and not soon removed on macOS.

Here’s a quick way to check your shell versions (except for dash):

macOSzshbash/shcsh/tcshkshdash
10.03.0.8 (2000-05-16)3.0.8 (zsh, no bash)6.08.00 (1998-10-02)
10.13.0.8 (2000-05-16)3.0.8 (zsh, no bash)6.10.00 (2000-11-19)
10.24.0.4 (2001-10-26)2.05b.0 (2002-07-17)6.10.00 (2000-11-19)
10.34.1.1 (2003-06-19)2.05b.0 (2002-07-17)6.12.00 (2002-07-23)
10.44.2.3 (2005-03-00)2.05b.0 (2002-07-17)6.12.00 (2002-07-23)M p (1993-12-28)
10.54.3.4 (2007-04-19)3.2.17 (2007-05-01)6.14.00 (2005-03-23)M s+ (1993-12-28)
10.64.3.4 (2008-11-03)3.2.48 (2008-11-18)6.15.00 (2007-03-03)M s+ (1993-12-28)
10.74.3.11 (2010-12-20)3.2.48 (2008-11-18)6.17.00 (2009-07-10)M s+ (1993-12-28)
10.84.3.11 (2010-12-20)3.2.48 (2008-11-18)6.17.00 (2009-07-10)JM 93u (2011-02-08)
10.95.0.2 (2012-12-12)3.2.51 (2010-03-17)6.17.00 (2009-07-10)JM 93u (2011-02-08)
10.105.0.5 (2014-01-06)3.2.57 (2014-11-07)6.17.00 (2009-07-10)AJM 93u+ (2012-08-01)
10.115.0.8 (2015-05-31)3.2.57 (2014-11-07)6.18.01 (2012-02-14)AJM 93u+ (2012-08-01)
10.125.2 (2015-12-02)3.2.57 (2014-11-07)6.18.01 (2012-02-14)AJM 93u+ (2012-08-01)
10.135.3 (2016-12-12)3.2.57 (2014-11-07)6.18.01 (2012-02-14)AJM 93u+ (2012-08-01)
10.145.3 (2016-12-12)3.2.57 (2014-11-07)6.18.01 (2012-02-14)AJM 93u+ (2012-08-01)
10.155.7.1 (2019-02-03)3.2.57 (2014-11-07)6.21.00 (2019-05-08)AJM 93u+ (2012-08-01)dash-9 (1993)

As you can see, zsh has has been updated with almost every new release of macOS. Bash really hit a wall with 3.2 and as many have noted, it was v4’s change in licensing to GPLv3 that caused this (sh is really bash in sh compatibility mode so the versions are intertwined). csh/tcsh has the same duality thing going on and took a notably giant 7 year leap in 10.15 to a version from 2019. ksh has remained at the same version just as long as bash yet I don’t think anyone is fretting that ksh will be deprecated or removed. Finally, dash is just a weirdo that apparently disdains versioning! I used a combination of what /bin/dash and man dash to get some sort of crude answer.

So there you go: In my opinion, all signs point to bash being yet another shell on macOS for some time. Removing bash from macOS would break a lot of stuff and while that reason alone hasn’t stopped Apple before, I think they will let sleeping dogs lie. Go ahead and learn zsh, master it, customize it, or make it “sexy” but take the rumors of its demise on macOS with a grain of salt and dose of skepticism.