Avoid Unicode Mangling in Jamf with shef

Computers are “the equivalent of a bicycle for our minds” as Steve Job once said. Sure enough, like a bicycle, they also require maintenance! Sometimes that requires soliciting the user to take action. Asking at the right time is very important (see my post Don’t Be a Jerk) and so is brevity. I’ve found a sprinkling of emoji and other symbols can help your message cut through the noise. A bright red stop sign πŸ›‘ can convey its meaning with only a glance, especially for non-native speakers of your language. Your tooling however, may trip you up and mangle your text.

While AppleScript, Swift Dialog and JamfHelper can all handle Unicode, Jamf databases tables by default do not support 4-byte Unicode! I touched on this in my post: jpt 1.0 text encoding fun. While the database engine might support it, chances are your tables are Latin1 which top out at 3-byte UTF-8 encoded characters. 4-byte characters get mangled. Let’s take a look at this in action:

Looks good while editing…
Once saved, the mangling is clear

Only the gear makes it through because it’s actually two 3-byte characters (U+2699 βš™ gear plus variation selector U+FE0F) but who has time to check which one is which? How about an encoding tool written in shell, that allows you to escape and format Unicode for shell scripts and/or Jamf script parameters that can make it through unscathed and un-mangled? Sound good? Great! I present shef, the Shell Encoder and Formatter!

Give it some text, specify an encoding scheme and/or quoting style and out comes your string ready for use! You can put the resulting string in your script as a variable or as a script parameter in a Jamf policy depending on quoting options. I’ve done the hard work of finding the various ways to escape special characters for shell and made this handy script for you!

shef Examples

First, let’s make a simple script for Jamf that processes the output of shef. I’ve chosen AppleScript so you can play along at home even if you don’t have Jamf installed, this can also be applied to “wrapper scripts” that leverage other tools like Swift Dialog or JamfHelper. At it’s heart is the simple technique of encoding the input with shef then decoding it in the script before presentation. If you use bash use echo -e if you use sh or zsh then just echo will work, it’s that simple. Our example script simpleAlert-AS.sh, keeps things small and as minimal as possible but keep in mind my fuller-featured tool shui for more robust scripts where you may need text or password entry, file picking, etc. without external dependencies but don’t want to bother learning AppleScript.

#simpleAlert-AS - Copyright (c) 2023 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

#Simple Applescript alert dialog for Jamf - just a title, a message and an OK button
#Accepts hex (\xnn) and octal (\0nnn) escaped UTF-8 encoded characters (since the default Jamf db character set mangles 4 byte Unicode)
#Use shef to encode your strings/files for use in this script: https://github.com/brunerd/shef

#function to interpret the escapes and fixup characters that can screw up Applescript if unescaped \ and "
function interpretEscapesFixBackslashesAndQuotes()(echo -e "${@}" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')

function jamflog(){
	local logFile="/var/log/jamf.log"
	#if we cannot write to the log or it does not exist, unset and tee simply echoes
	[ ! -w "${logFile}" ] && unset logFile
	#this will tee to jamf.log in the jamf log format: <Day> <Month> DD HH:MM:SS <Computer Name> ProcessName[PID]: <Message>
	echo "$(date +'%a %b %d %H:%M:%S') ${myComputerName:="$(scutil --get ComputerName)"} ${myName:="$(basename "${0}" | sed 's/\..*$//')"}[${myPID:=$$}]: ${1}" | tee -a "${logFile}" 2>/dev/null

#process our input then escape for AppleScript
message=$(interpretEscapesFixBackslashesAndQuotes "${4}")
title=$(interpretEscapesFixBackslashesAndQuotes "${5}")
#could be a path or a built-in icon (stop, caution, note)
#invoke the system open command with this argument (URL, preference pane, etc...)

#these are the plain icons (Applescript otherwise badges them with the calling app)
case "${icon}" in
	"stop") icon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns";;
	"caution") icon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertCautionIcon.icns"
		#previous icon went away in later macOS RIP
		[ ! -f "${icon}" ] && icon="/System/Library/CoreServices/Problem Reporter.app/Contents/Resources/ProblemReporter.icns";;
	"note") icon="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertNoteIcon.icns";;

#make string only if path is valid (otherwise dialog fails)
if [ -f "${icon}" ]; then
	withIcon_AS="with icon file (POSIX file \"${icon}\")"

jamflog "Prompting user: $(stat -f %Su /dev/console)"

#prompt the user, giving up and moving on after 1 day (86400 seconds)
/usr/bin/osascript <<-EOF
with timeout of 86400 seconds
	display dialog "${message}" with title "${title}" ${withIcon_AS} buttons {"OK"} default button "OK" giving up after "86400"
end timeout

if [ -n "${open_item}" ]; then
	jamflog "Opening: ${open_item}"
	open "${open_item}"

exit 0

Upload the above script to your Jamf (if you have one), label parameters 4, 5, 6, and 7 as: Message, Title, Icon Path, an Open After OK, respectively. If running local keep this in mind and put 1, 2, 3 as place holder arguments for the first three parameters.

Jamf Script parameters names for

Let’s come up with an example message for our users. How about the common refrain coming from MacAdmins around the world:

πŸ›‘ Stop.
βš™οΈ Run your updates.
πŸ™ Thanks!

Now if I tried to pass this text to a script within a Jamf policy, all those emoji would get mangled by Jamf as we saw above. Let’s use shef to encode the string for Jamf:

% shef <<'EOF'
heredoc> πŸ›‘ Stop.
heredoc> βš™οΈ Run your updates.
heredoc> πŸ™ Thanks!
heredoc> EOF
\xF0\x9F\x9B\x91 Stop.\n\xE2\x9A\x99\xEF\xB8\x8F Run your updates.\n\xF0\x9F\x99\x8F Thanks!

For the example above I am using a “here-doc”; in practice you can simply supply shef a file path. The output encodes all the newlines in the ANSI-C style of \n and the emoji have all been replaced by their UTF-8 encodings using the hexadecimal escaping of \x. We can take this output and use it in our Jamf policy. The message will get through both textually and symbolically. As an additional feature bonus I added a simple open action in the script. Supply the file path to the Software Update panel and after the user clicks OK, it will be opened. Scope a policy like this to anyone with pending updates with Daily frequency:

Escape the mangling in Jamf!

If you are just running the script locally and calling from the Terminal you’ll want to specify a quoting option. Exclamations are tricky and shells usually love to interpret trailing exclamations as a history expansion command, shef does its best to avoid this:

bash-3.2$ shef -Qd <<'EOF'
> πŸ›‘ Stop.
> βš™οΈ Run your updates.
> πŸ™ Thanks!
"\xF0\x9F\x9B\x91 Stop.\n\xE2\x9A\x99\xEF\xB8\x8F Run your updates.\n\xF0\x9F\x99\x8F Thanks"\!""

bash-3.2$ ./simpleAlert-AS.sh 1 2 3 "\xF0\x9F\x9B\x91 Stop.\n\xE2\x9A\x99\xEF\xB8\x8F Run your updates.\n\xF0\x9F\x99\x8F Thanks"\!"" "Software Updates Pending" stop "/System/Library/PreferencePanes/SoftwareUpdate.prefPane"
Our un-mangled output

Stop the Mangling Madness!

Wrapping things up: use shef to encode strings with Unicode so they survive storage in Jamf’s Latin1 encoded db tables. shui my fuller featured AppleScript dialog tool is ready to accept shef encoded strings. You can also use shef minify your text for your shell script. In fact, I used it to encode the help text file down to a single quoted line for use within itself. That’s 1 happy customer and counting, hope you find it useful too!

Determining iCloud Drive and Desktop and Documents Sync Status in macOS

I’m on a roll with iCloud stuff. In this post I’d like to show you how you can determine if either iCloud Drive is enabled along with the “Desktop and Documents Folders” sync feature. While you can use MDM to turn these off, perhaps you like to know who you’d affect first! Perhaps the folks in your Enterprise currently using these features are in the C-Suite? I’m sure they’d appreciate a heads up before all their iCloud docs get removed from their Macs (when MDM disallowance takes affect it is swift and unforgiving).

iCloud Drive Status

When it comes to using on-disk artifacts to figure out the state of macOS I like to use the analogy of reading tea leaves. Usually it’s pretty straightforward but every now and then there’s something inscrutable and you have to take your best guess. For example iCloud Drive status is stored in your home folder at ~/Library/Preferences/MobileMeAccounts.plist but yet the Accounts key is an array. The question is how and why you could even have more than one iCloud account signed-in?! Perhaps a reader will tell me when and how you would ever have more than one? For now it seems inexplicable.

Update: It seems quite obvious now but as many folks did point out, of course you can add another iCloud account to Internet Accounts and it can sync Mail, Contacts, Calendars, Reminders, and Notes, just not iCloud Drive. Only one iCloud Drive user per user on a Mac. While I do have another AppleID I only use it for Media and Purchases and none of the other iCloud services. The code below stays the same as it is looking at all array entries. Aside: Boy, do I wish I could merge or transfer my old iTunes purchasing/media Apple ID with my main iCloud Apple ID! <weakly shakes fist at faceless Apple bureaucracy that for some reason hasn't solved the problem of merging Apple IDs>

Regardless of that mystery jpt can use the JSONPath query language to get us an answer in iCloudDrive_func.sh and the minified iCloudDrive_func.min.sh. Below is an edited excerpt:

#Joel Bruner - iCloudDrive.func.sh - gets the iCloud Drive status for a console user


function iCloudDrive()(

	#for brevity pretend we've pasted in the minified jpt function:
	#for the full function see https://github.com/brunerd/macAdminTools/tree/main/Scripts

	consoleUser=$(stat -f %Su /dev/console)

	#if root grab the last console user
	if [ "${consoleUser}" = "root" ]; then
		consoleUser=$(/usr/bin/last -1 -t console | awk '{print $1}')

	userPref_json=$(sudo -u $consoleUser defaults export MobileMeAccounts - | plutil -convert json - -o -)

	#pref domain not found an empty object is returned
	if [ "${userPref_json}" = "{}" ]; then
		return 1
		#returns the number paths that match
		matchingPathCount=$(jpt -r '$.Accounts[*].Services[?(@.Name == "MOBILE_DOCUMENTS" && @.Enabled == true)]' <<< "${userPref_json}" 2>/dev/null | wc -l | tr -d "[[:space:]]")
		if [ ${matchingPathCount:=0} -eq 0 ]; then
			return 1
			return 0
# MAIN #

#example function usage, if leverages the return values
if iCloudDrive; then
	echo "iCloud Drive is ON"
	exit 0
	echo "iCloud Drive is OFF"
	exit 1

The magic happens once we’ve gotten the JSON version of MobileMeAccount.plist I use jpt to see if there are any objects within the Accounts array with Services that have both a Name that matches MOBILE_DOCUMENTS and have an Enabled key that is set to true, the -r option on jpt tells it to output the the JSON Pointer path(s) the query matches. I could have used the -j option to output JSONPath(s) but either way a line is a line and that’s all we need. Altogether it looks like this: jpt -r '$.Accounts[*].Services[?(@.Name == "MOBILE_DOCUMENTS" && @.Enabled == true)]

Again because Accounts is an Array we have it look at all of them with $.Accounts[*]and in the off chance we get more than one we simply say if the number of matches is greater than zero then it’s on. This works very well in practice. This function could best be used as a JAMF Extension Attribute. I’ll leave that as a copy/paste exercise for the reader. Add it to your Jamf Pro EAs, let sit for 24-48 hours and check for results! ⏲ And while some of you might balk at a 73k Extensions Attribute, the execution time is on average a speedy .15s!

#a pretend iCloudDrive Jamf EA 
#pretend we've pasted in the function iCloudDrive() above

if iCloudDrive; then

echo "<result>${result}</result>"

iCloud Drive “Desktop and Documents” status

Thankfully, slightly easier to determine yet devilishly subtle to discover, is determining the status of the “Desktop and Documents” sync feature of iCloud Drive. After searching in vain for plist artifacts, I discovered the clue is in the extended attributes of your Desktop (and/or Documents) folder! You can find the scripts at my GitHub here iCloudDriveDesktopSync_func.sh and the minified version iCloudDriveDesktopSync_func min.sh

#Joel Bruner - iCloudDriveDesktopSync - gets the iCloud Drive Desktop and Document Sync Status for the console user


#must be run as root
function iCloudDriveDesktopSync()(
	consoleUser=$(stat -f %Su /dev/console)

	#if root (loginwindow) grab the last console user
	if [ "${consoleUser}" = "root" ]; then
		consoleUser=$(/usr/bin/last -1 -t console | awk '{print $1}')

	#if this xattr exists then sync is turned on
	xattr_desktop=$(sudo -u $consoleUser /bin/sh -c 'xattr -p com.apple.icloud.desktop ~/Desktop 2>/dev/null')

	if [ -z "${xattr_desktop}" ]; then
		return 1
		return 0

#example function usage, if leverages the return values
if iCloudDriveDesktopSync; then
	echo "iCloud Drive Desktop and Documents Sync is ON"
	exit 0
	echo "iCloud Drive Desktop and Documents Sync is OFF"
	exit 1

The operation is pretty simple, it finds a console user or last user, then runs the xattr -p command as that user (anticipating this being run as root by Jamf) to see if the com.apple.icloud.desktop extended attribute exists on their ~/Desktop. In testing you’ll find if you toggle the “Desktop and Documents” checkbox in the iCloud Drive options, it will apply this to both of those folders almost immediately without fail. The function can be used in a Jamf Extension Attribute in the same way the iCloudDrive was above. Some assembly required. πŸ’ͺ

So, there you go! Another couple functions to read the stateful tea leaves of iCloud Drive settings. Very useful if you are about to disallow iCloud Drive and/or Desktop and Document sync via MDM but need to know who you are going to affect and let them know beforehand. Because I still stand by this sage advice: Don’t Be A Jerk. Thanks for reading you can find these script and more at my GitHub repo. Thanks for reading!

brunerd tools summer 2022 updates

Hey there, it’s summer 2022 and I wanted to share some updates on my shell tools for Mac admins.

No breaking changes or anything huge, but some nice incremental improvements all the same. When the JSONPath spec is finalized I’ll give jpt a more considerable under the hood improvement. For now though, I’m pretty happy with tweaking their behaviors to act like you’d expect (print help and exit if no input detected), to get out of your way when you need to debug your scripts (xtrace is now disabled inside them) and to try to understand what you mean when you ask them a question (key path queries now accepted). Because the tools and technologies we make should serve us, not the other way around. I hope these tools can save you time, energy, sanity or all the above!

β€’ jpt the “JSON Power Tool” has been updated to v1.0.2 with all the nice additions mentioned. If you don’t know already jpt can parse, query, and modify JSON every which way. It can be used as both a standalone utility as well as a function to be embedded into your shell scripts, without any other dependencies.

β€’ ljt the “little JSON tool” is jpt‘s smaller sibling weighing in at only 2k. While it lacks the transformational powers of jpt it can easily pluck a value out of JSON up to 2GB big and output results up to 720MB. It has been updated to v1.0.7 because dang it, even a small script can have just as many bugs as the big ones. Sometimes more if you’re not careful!

Both jpt and ljt now accept ye olde NextStep “keypath” expressions to help win over masochists who might still use plutil to work with JSON! 😁 (I kid, I kid!)

β€’ jse, the “JSON string encoder” does this one thing and does it well. It can be used in conjunction with jpt when taking in arbitrary data that needs encoding only. It’s a svelte 1.4k (in minified form) and runs considerably faster that jpt, so it’s ideal for multiple invocations, such as when preparing input data for a JSON Patch document. v1.0.2 is available here

β€’ shui lets you interact with your users with AppleScript but in the comfort of familiar shell syntax! You don’t need to know a line of AppleScript, although it does have the option to output it if you want to learn. Save time, brain cells and hair with shui, while you keep on trucking in shell script. It has been updated to v20220704.

P.S. All these tools still work in the macOS Ventura beta πŸ˜…

Respecting Focus and Meeting Status in Your Mac scripts (aka Don’t Be a Jerk)

In four words Barbara Krueger distills the Golden Rule into an in-your-face admonishment: Don’t be a jerk. It makes for a great coffee mug but how does this relate to shell scripting for a Mac admin and engineer? Well, sometimes a script can only go so far and you need user consent and cooperation to get the job done. Not being a jerk about it can pay off!

A good way to get cooperation from your users is to build upon a foundation of trust and respect. While IT has a long list of to-dos for users: “Did you reboot? Did you run updates? Did you open a ticket? Did you really reboot?” the users might have one for us: “Respect our screens when we’re in the middle of a client meeting and respect when Focus mode is turned on.” And they are right. That’s also two things. “Give an inch and they take a mile” I tell ya!

So how can we in IT can be considerate of our users? First, don’t do anything Nick Burns from SNL does. Second, use the functions below (and in my GitHub) to check if a Zoom, Teams, Webex, or Goto meeting is happening or if Do Not Disturb/Focus mode is on. When non-user-initiated scripts (i.e. daily pop-ups/nags/alerts) run they can bail or wait if the user is busy. If it were real life (and it is!) we wouldn’t walk into a meeting and bug a user, in front of everyone, to run update. If we did, they’d be more likely to remember how rude we were rather than actually running the updates. So let’s get their attention when they will be most receptive to what we have to say.

Detecting Online Meetings Apps

First up is inMeeting_Zoom which simply checks for the CptHost process and returns success or fail. Notice how this simple behavior can be used with an if/then statement. The return code is evaluated by the if, a zero is success and a non-zero is a failure. && is a logical AND and || is a logical OR

#inMeeting_Zoom (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Zoom {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	pgrep "CptHost" &>/dev/null && return 0 || return 1

if inMeeting_Zoom; then
	echo "In Zoom meeting... don't be a jerk"
	echo "Not in Zoom meeting"

Next is another process checker for Webex: inMeetng_Webex. What is a bit more unique is the process appears in ps in parentheses as (WebexAppLauncher)however pgrep cannot find this process (because the actual name has been rewritten by the Meeting Center process). We instead use a combination of ps and grep. A neat trick with grep is to use a [] regex character class to surround a single character, this keeps grep from matching itself in the process list. That way you don’t need to have an extra grep -v grep to clean up the output.

#inMeeting_Webex (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Webex {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	ps auxww | grep -q "[(]WebexAppLauncher)" && return 0 || return 1

if inMeeting_Webex; then
	echo "In Zoom meeting... don't be a jerk"
	echo "Not Webex in meeting"

Goto Meeting is more straightforward, although it should be noted that regardless of quote type, single or double, the parentheses must be escaped with a backslash. Otherwise, it’s the same pattern, look for the process name which only appears during a meeting or during the meeting preview and return 0 or 1 for if to evaluate, find it here: inMeeting_Goto

#inMeeting_Goto (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Goto() {
	#if this process exists, there is a meeting, return 0 (sucess), otherwise 1 (fail)
	pgrep "GoTo Helper \(Plugin\)" &>/dev/null && return 0 || return 1

if inMeeting_Goto; then
	echo "In Goto meeting... don't be a jerk"
	echo "Not in Goto meeting"

Lastly, Teams is a bit more complex, rather than looking for the presence of a process, we instead look for a JSON file in the user’s /Library/Application Support/Microsoft/Teams folder which has the current call status for both the app and the web plugin (the other methods above are for the app only). We’ll use the ljt to extract the value from the JSON. In fact I wrote ljt after starting to write this blog last week and realizing that jpt (weighing in at 64k) was just overkill. As a bonus to doing that, I just realized that bash functions can contain functions! Long ago I ditched using () in shell function declarations and just used the function keyword. Empty parentheses seemed decorative rather than functional since it’s not like it’s a C function that needs parameter names and types. However the lack of parentheses () apparently, prevents a function from being declared inside a function! Below I just wanted to make sure ljt doesn’t get separated from inMeetings_Teams

#inMeeting_Teams (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

function inMeeting_Teams ()(

	function ljt () ( #v1.0.3
		[ -n "${-//[^x]/}" ] && set +x; read -r -d '' JSCode <<-'EOT'
		try {var query=decodeURIComponent(escape(arguments[0]));var file=decodeURIComponent(escape(arguments[1]));if (query[0]==='/'){ query = query.split('/').slice(1).map(function (f){return "["+JSON.stringify(f)+"]"}).join('')}if(/[^A-Za-z_$\d\.\[\]'"]/.test(query.split('').reverse().join('').replace(/(["'])(.*?)\1(?!\\)/g, ""))){throw new Error("Invalid path: "+ query)};if(query[0]==="$"){query=query.slice(1,query.length)};var data=JSON.parse(readFile(file));var result=eval("(data)"+query)}catch(e){printErr(e);quit()};if(result !==undefined){result!==null&&result.constructor===String?print(result): print(JSON.stringify(result,null,2))}else{printErr("Node not found.")}
		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 { 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

	consoleUser=$(stat -f %Su /dev/console)
	consoleUserHomeFolder=$(dscl . -read /Users/"${consoleUser}" NFSHomeDirectory | awk -F ': ' '{print $2}')
	storageJSON_path="${consoleUserHomeFolder}/Library/Application Support/Microsoft/Teams/storage.json"
	#no file, no meeting
	[ ! -f "${storageJSON_path}" ] && return 1

	#get both states
	appState=$(ljt /appStates/states "${storageJSON_path}" | tr , $'\n' | tail -n 1)
	webappState=$(ljt /webAppStates/states "${storageJSON_path}"| tr , $'\n' | tail -n 1)
	#determine app state
	if [ "${appState}" = "InCall" ]	|| [ "${webAppState}" = "InCall" ]; then
		return 0
		return 1

if inMeeting_Teams; then
	echo "In Teams Meeting... don't be a jerk"
	echo "Not in Teams Meeting"

Detecting Focus (formerly Do Not Disturb)

Last but not least is determining Focus (formerly Do Not Disturb) with doNotDisturb. As you can see there’s been a few different ways this has been implemented over the years. In macOS 10.13-11 the state was stored inside of a plist. For macOS 12 Monterey they’ve switched from a plist to a JSON file. A simple grep though is all that’s needed to find the key name storeAssertionRecords. If it is off, that string is nowhere to be find, when it’s on it’s there. Simple (as in Keep it Simple Stoopid)

#doNotDisturb (grep) (20220227) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#Licensed under the MIT License

#An example of detecting Do Not Disturb (macOS 10.13-12)

function doNotDisturb()(

	OS_major="$(sw_vers -productVersion | cut -d. -f1)"
	consoleUserID="$(stat -f %u /dev/console)"
	consoleUser="$(stat -f %Su /dev/console)"
	#get Do Not Disturb status
	if [ "${OS_major}" = "10" ]; then
		#returns c-cstyle boolean 0 (off) or 1 (on)
		dndStatus="$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} defaults -currentHost read com.apple.notificationcenterui doNotDisturb 2>/dev/null)"

		#eval c-style boolean and return shell style value
		[ "${dndStatus}" = "1" ] && return 0 || return 1
	#this only works for macOS 11 - macOS12 does not affect any of the settings in com.apple.ncprefs
	elif [ "${OS_major}" = "11" ]; then
		#returns "true" or [blank]
		dndStatus="$(/usr/libexec/PlistBuddy -c "print :userPref:enabled" /dev/stdin 2>/dev/null <<< "$(plutil -extract dnd_prefs xml1 -o - /dev/stdin <<< "$(launchctl asuser ${consoleUserID} sudo -u ${consoleUser} 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}" ] && return 0 || return 1
	elif [ "${OS_major}" -ge "12" ]; then
		consoleUserHomeFolder=$(dscl . -read /Users/"${consoleUser}" NFSHomeDirectory | awk -F ': ' '{print $2}')

		#if Assertions.json file does NOT exist, then DnD is OFF
		[ ! -f "${file_assertions}" ] && return 1

		#simply check for storeAssertionRecords existence, usually found at: /data/0/storeAssertionRecords (and only exists when ON)
		! grep -q "storeAssertionRecords" "${file_assertions}" 2>/dev/null && return 1 || return 0
if doNotDisturb; then
	echo "DnD/Focus is ON... don't be a jerk"
	echo "DnD/Focus is OFF"

Since Focus can remain on indefinitely an end user may never see your pop-up. If so, build a counter with a local plist to record and increment the number of attempts. After a threshold has been reached you can then break through to the user (I certainly do).

Detecting Apps in Presentation Mode

A newer addition to this page is some code to detect fullscreen presentation apps that I cannot take credit for. Adam Codega, one of the contributors to Installomator hipped me to a cool line of code that was added in PR 268. It leverages pmset to see what assertions have been made to the power management subsystem. It uses awk to look for the IOPMAssertionTypes named NoDisplaySleepAssertion and PreventUserIdleDisplaySleep with some additional logic to throw out false positives from coreaudiod. In testing I’ve found this able to detect the presentation modes of Keynote, Powerpoint and Google Slides in Slideshow mode in Chrome (but not Safari), your mileage may vary for other apps. Another caveat is that when a YouTube video is playing in a visible tab, it will assert a NoDisplaySleepAssertion, however these will be named “Video Wake Lock” whereas a Slideshow presentation mode will have its name assertions named “Blink Wake Lock”. So I am adding an additional check to throw our “Video Wake Locks”. This may be more of a can of worms than you’d like, if so, user education to set Focus mode may be the way to go. A functionalized version can be found here: inPresentationMode

#inPresentationMode (20220319) Copyright (c) 2022 Joel Bruner (https://github.com/brunerd)
#with code from Installomator (PR 268) (https://github.com/Installomator/Installomator) Copyright 2020 Armin Briegel
#Licensed under the MIT License

function inPresentationMode {
	#Apple Dev Docs: https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes
	#ignore assertions without the process in parentheses, any coreaudiod procs, and "Video Wake Lock" is just Chrome playing a Youtube vid in the foreground
	assertingApps=$(/usr/bin/pmset -g assertions | /usr/bin/awk '/NoDisplaySleepAssertion | PreventUserIdleDisplaySleep/ && match($0,/\(.+\)/) && ! /coreaudiod/ && ! /Video\ Wake\ Lock/ {gsub(/^.*\(/,"",$0); gsub(/\).*$/,"",$0); print};')
	[ -n "${assertingApps}" ] && return 0 || return 1

if inPresentationMode; then
	echo "In presentation mode... don't be a jerk"
	echo "Not in presentation mode..."

All together now

Putting it all together here’s how you can test multiple functions in a single if statement, just chain them together with a bunch of || ORs

#Joel Bruner - demo of meeting/focus aware functions for your script

#pretend we've declared all the functions above and copy and pasted them in here
function doNotDisturb()(:)
function inMeeting_Teams()(:)
function inMeeting_Zoom(){:}
function inMeeting_Goto(){:}
function inMeeting_Webex(){:}
function inPresentationMode(){:}

#test each one with || OR conditionals
#the FIRST successful test will "short-circuit" and no more functions will be run
if doNotDisturb || inPresentationMode || inZoomMeeting || inMeeting_Goto || inMeeting_Webex || inMeeting_Teams; then
	echo "In a meeting, presentation, or Focus is On... don't be a jerk"
	#do something else, like wait 
	echo "Not in a meeting..."
	#alert the user or do whatever you needed to do that might impact them

Using these functions in your scripts can help respect your users’ online meeting times and Focus states. Also it doesn’t hurt to document it somewhere and toot your own horn in a user facing KB or wiki. If and when a user complains about that pop-up that destroyed their concentration and their world, you can show them the forethought and effort you’ve taken to be as considerate as possible regarding this perceived incursion. This usually has the effect of blowing their mind 🀯 that someone in IT is actually trying to be considerate!

P.S. I’m pretty stoked that Prism.js can really jazz up my normally dreary grey code blocks! πŸ˜πŸ€“ I found a good WordPress tutorial here

ljt, a little JSON tool for your shell script

ljt, the Little JSON Tool, is a concise and focused tool for quickly getting values from JSON and nothing else. It can be used standalone or embedded in your shell scripts. It requires only macOS 10.11+ or a *nix distro with jsc installed. It has no other dependencies.

You might have also seen my other project jpt, the JSON Power Tool. It too can be used standalone or embedded in a script however its features and size (64k) might be overkill in some case and I get it! I thought the same thing too after I looked at work of Mathew Warren, Paul Galow, and Richard Purves. Sometimes you don’t need to process JSON Text Sequences, use advanced JSONPath querying, modify JSON, or encode the output in a myriad of ways. Maybe all you need is something to retrieve a JSON value in your shell script.

Where jpt was an exercise in maximalism, ljt is one of essential minimalism – or at least as minimal as this CliftonStrengths Maximizer can go! πŸ€“ The minified version is mere 1.2k and offers a bit more security and functionality than a one-liner.

ljt features:
β€’ Query using JSON Pointer or JSONPath (canonical only, no filters, unions, etc)
β€’ Javascript code injection prevention for both JSON and the query
β€’ Multiple input methods: file redirection, file path, here doc, here string and Unix pipe
β€’ Output of JSON strings as regular text and JSON for all others
β€’ Maximum input size of 2GB and max output of 720MB (Note: functions that take JSON data as an environment variable or an argument are limited to a maximum of 1MB of data)
β€’ Zero and non-zero exit statuses for success and failure, respectively

Swing by the ljt Github page and check it out. There are two versions of the tool one is fully commented for studying and hacking (ljt), the other is a “minified” version without any comments meant for embedding into your shell scripts (ljt.min). The Releases page has a macOS pkg package to install and run ljt as a standalone utility.

Thanks for reading and happy scripting!

jpt 1.0 text encoding fun

Besides JSON, jpt (the JSON power tool) can also output strings and numbers in a variety of encodings that the sysadmin or programmer might find useful. Let’s look at the encoding options from the output of jpt -h

% jpt -h
-T textual output of all data (omits property names and indices)
	-e Print escaped characters literally: \b \f \n \r \t \v and \\ (escapes formats only)
	-i "<value>" indent spaces (0-10) or character string for each level of indent
	-n Print null values as the string 'null' (pre-encoding)

	-E "<value>" encoding options for -T output:

	  Encodes string characters below 0x20 and above 0x7E with pass-through for all else:
		x 	"\x" prefixed hexadecimal UTF-8 strings
		O 	"\nnn" style octal for UTF-8 strings
		0 	"\0nnn" style octal for UTF-8 strings
		u 	"\u" prefixed Unicode for UTF-16 strings
		U 	"\U "prefixed Unicode Code Point strings
		E 	"\u{...}" prefixed ES2016 Unicode Code Point strings
		W 	"%nn" Web encoded UTF-8 string using encodeURI (respects scheme and domain of URL)
		w 	"%nn" Web encoded UTF-8 string using encodeURIComponent (encodes all components URL)

		  -A encodes ALL characters
	  Encodes both strings and numbers with pass-through for all else:
		h 	"0x" prefixed lowercase hexadecimal, UTF-8 strings
		H 	"0x" prefixed uppercase hexadecimal, UTF-8 strings
		o 	"0o" prefixed octal, UTF-8 strings
		6 	"0b" prefixed binary, 16 bit _ spaced numbers and UTF-16 strings
		B 	"0b" prefixed binary, 8 bit _ spaced numbers and UTF-16 strings
		b 	"0b" prefixed binary, 8 bit _ spaced numbers and UTF-8 strings

		  -U whitespace is left untouched (not encoded)


While the above conversion modes will do both number and string types, these options will work only on strings (numbers and booleans pass-through). If you work with with shell scripts these techniques may be useful.

If you store shell scripts in a database that’s not using utf8mb4 table and column encodings then you won’t be able to include snazzy emojis to catch your user’s attention! In fact this WordPress install was so old (almost 15 years!) the default encoding was still latin1_swedish_ci, which an odd but surprisingly common default for many old blogs. Also if you store your scripts in Jamf (still in v10.35 as of this writing) it uses latin1 encoding and your 4 byte characters will get mangled. Below you can see in Jamf things look good while editing, fails once saved, and the eventual workaround is to use an coding like \x escaped hex (octal is an alternate)

Let’s use the red “octagonal sign” emoji, which is a stop sign to most everyone around the world, with the exception of Japan and Libya (thanks Google image search). Let’s look at some of the way πŸ›‘ can be encoded in a shell script

#reliable \x hex notation for bash and zsh
% jpt -STEx <<< "Alert πŸ›‘"
Alert \xf0\x9f\x9b\x91

#above string can be  in both bash and zsh
% echo $'Alert \xf0\x9f\x9b\x91'
Alert πŸ›‘

#also reliable, \nnn octal notation
% jpt -STEO <<< "Alert πŸ›‘"
Alert \360\237\233\221

#works in both bash and zsh
% echo $'Alert \360\237\233\221'
Alert πŸ›‘

#\0nnn octal notation
% jpt -STE0 <<< "Alert πŸ›‘"
Alert \0360\0237\0233\0221

#use with shell builtin echo -e and ALWAYS in double quotes
#zsh does NOT require -e but bash DOES, safest to use -e
% echo -e "Alert \0360\0237\0233\0221"
Alert πŸ›‘

#-EU code point for zsh only
% jpt -STEU <<< "Alert πŸ›‘"
Alert \U0001f6d1

#use in C-style quotes in zsh
% echo $'Alert \U0001f6d1'
Alert πŸ›‘

The -w/-W flags can encode characters for use in URLs

#web/percent encoded output in the case of non-URLs -W and -w are the same
% jpt -STEW <<< πŸ›‘  

#-W URL example (encodeURI)
jpt -STEW <<< http://site.local/page.php?wow=πŸ›‘

#-w will encode everything (encodeURIComponent)
% jpt -STEw <<< http://site.local/page.php?wow=πŸ›‘

And a couple other oddballs…

#text output -T (no quotes), -Eu for \u encoding
#not so useful for the shell scripter
#zsh CANNOT handle multi-byte \u character pairs
% jpt -S -T -Eu <<< "Alert πŸ›‘"
Alert \ud83d\uded1

#-EE for an Javascript ES2016 style code point
% jpt -STEE <<< "Alert πŸ›‘"
Alert \u{1f6d1}

You can also \u encode all characters above 0x7E in JSON with the -u flag

#JSON output (not using -T)
% jpt <<< '"Alert πŸ›‘"'
"Alert πŸ›‘"

#use -S to treat input as a string without requiring hard " quotes enclosing
% jpt -S <<< 'Alert πŸ›‘'
"Alert πŸ›‘"

#use -u for JSON output to encode any character above 0x7E
% jpt -Su <<< 'Alert πŸ›‘'
"Alert \ud83d\uded1"

#this will apply to all strings, key names and values
% jpt -u <<< '{"πŸ›‘":"stop", "message":"Alert πŸ›‘"}' 
  "\ud83d\uded1": "stop",
  "message": "Alert \ud83d\uded1"

Whew! I think I covered them all. If there are newlines, tabs and other invisibles you can choose to output them or leave them encoded when you are outputting to text with -T

#JSON in, JSON out
jpt <<< '"Hello\n\tWorld"'

#ANSI-C string in, -S to treat as string despite lack of " with JSON out
% jpt -S <<< $'Hello\n\tWorld' 

#JSON in, text out: -T alone prints whitespace characters
% jpt -T <<< '"Hello\n\tWorld"'

#use the -e option with -T to encode whitespace
% jpt -Te <<< '"Hello\n\tWorld"'


Let’s start simply with some numbers. First up is hex notation in the style of 0xhh and 0XHH. This encoding has been around since ES1, use the -Eh and -EH respectively to do so. All alternate output (i.e. not JSON) needs the -T option. In shell you can combine multiple options/flags together except only the last flag can have an argument, like -E does below.

#-EH uppercase hex
% jpt -TEH <<< [255,256,4095,4096] 

#-Eh lowecase hex
% jpt -TEh <<< [255,256,4095,4096]

Next up are ye olde octals. Use the -Eo option to convert numbers to ye olde octals except using the more modern 0o prefix introduced in ES6

-Eo ES6 octals
% jpt -TEo <<< [255,256,4095,4096]    

Binary notation debuted in the ES6 spec, it used a 0b prefix and allows for _ underscore separators

#-E6 16 bit wide binary
% jpt -TE6 <<< [255,256,4095,4096]

#-EB 16 bit minimum width with _ separator per 8
% jpt -TEB <<< [255,256,4095,4096]

#-Eb 8 bit minimum width with _ separator per 8
% jpt -TEb <<< [15,16,255,256,4095,4096]

If you need to encode strings or numbers for use in scripting or programming, then jpt might be a handy utility for you and your Mac and if your *nix has jsc then it should work also. Check the jpt Releases page for Mac installer package download.

jpt 1.0, the JSON power tool in 2022

I’m happy to announce that jpt, the JSON power tool, has been updated to 1.0 and is available on my GitHub Releases page! It’s been over a year since the last release and I’ve been of working on, learning, and pondering what a really useful JSON tool could be and then working like heck to make it a reality.

jpt is a command line utility for formatting, querying, and modifying JSON. It can run on any version of macOS and most any Unix/Linux with jsc (JavaScriptCore) installed, otherwise it’s dependency free! For systems administrators and MacAdmins who need first class JSON tooling in their own shell scripts it can be embedded as a shell function in either bash or zsh. jpt was built to stand the test of time, needing only jsc and a shell, it will work on the newest macOS all the way back to OS X 10.4 Tiger!

There’s a year’s worth of bug fixes, improvements, and new features, let’s take a look at some of them.

Notable Features and Improvement

β€’Β jpt can now input and output multi-JSON documents like JSON Text Sequences (RFC 7464) plus other non-standard formats like NDJSON and JSON Lines and concatenated JSON. See: jpt 1.0 can deal with multiple JSON texts

β€’ Truncated JSON can be de detected in concatenated/lines JSON and repaired by closing up open strings, arrays and objects. See: Helping truncated JSON data with jpt 1.0

β€’ jpt can now fully rehabilitate JSON5 back to standard JSON. As I learned last year, there’s more mutant JSON than I realized! See: jpt 1.0 and JSON5 rehab

β€’ jpt can query JSON using both JSON Pointer, IETF RFC 6901 and JSONPath, a nascent draft RFC with powerful recursive search and filtering features.

β€’ jpt can manipulate JSON using JSONPatch, JSON Merge Patch, and some new merge modes not found anywhere else. See: merging JSON objects with jpt 1.0

β€’ shell scripters and programmers may find it useful that jpt has extensive string and number encoding capabilities. From classic encodings like hex, octal, and binary to more recent ones like URL/”percent” encoding and Unicode code points. See: jpt 1.0 text encoding fun

β€’ Need help finding visualizing JSON in new ways? jpt can output the structure of JSON in ways not seen, like JSONPath Object Literals which are simply the JSONPath, an equal sign, and the JSON value (e.g. $.ok="got it"). This simple and powerful declarative syntax can help find the exact path to a particular value and can also be used to quickly prototype a JSON object. See: jpt: see JSON differently

Try it out

There’s a lot to explore in jpt 1.0, download it from my GitHub with a macOS installer package in the Releases page. Check out my other jpt blog entries here at brunerd. As the IETF JSONPath standard takes shape I’ll be updating jpt to accomodate that standard. Until then, I hope this tool helps you in your JSON work, play, or research!

jpt: jamf examples pt. 2

Over in the MacAdmins’ #bash channel I saw a I question regarding how to get the Sharing states of Bluetooth devices from system_profiler. The most succinct answer was to awk out the values:

system_profiler SPBluetoothDataType 2> /dev/null | awk '/State: / {print $2}'

If you are using this for a Jamf Extension Attribute, I suppose it’ll do if you never want to allow any of them to be Enabled, but what if Internet Sharing was OK but not File Sharing? How would you match your Smart Group to multiple lines of unlabeled values? How would you match the first two but not the last two… and what if there was another USB Bluetooth device, that would add extra rows. Hmmm…

The answer for me, outputting the service name and the state on the same line. Since there isn’t a consistent line count from State: going back the service name, using something like grep -B n to include n lines of preceding data isn’t going to work.

          Bluetooth File Transfer:
              Folder other devices can browse: ~/Public
              When receiving items: Accept all without warning
              State: Disabled
          Bluetooth File Exchange:
              Folder for accepted items: ~/Downloads
              When other items are accepted: Save to location
              When receiving items: Accept all without warning
              State: Disabled
          Bluetooth Internet Sharing:
              State: Disabled

So you know what I say the answer to that is? That’s right, jpt the JSON Power Tool! It can parse the -json output from system_profiler in a more structured way and it allows for the discovery of as many applicable Bluetooth devices might be on the system.

Here’s a sample run with Internet Sharing turned On as well as Bluetooth Sharing turned On

file_browsing: disabled
object_push: enabled
internet_sharing: enabled

File Browsing is set to “Never Allow” but File Receiving is in the affirmative (Accept and Open, Accept and Save, or Ask). The addition of labels gives us the ability to create a Smart Group to match specific services like “file_browing: enabled” or any other combination thereof (perhaps internet_sharing should always be enabled, who am I to say what your requirements are!).

About the jpt

The JSON Power Tool (jpt) is a parser/manipulator for JSON documents written in Javascript and shell and can run standalone or embedded in your scripts bash or zsh and all the way back to OS X 10.4 Tiger! Check it out at: https://github.com/brunerd/jpt

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!