introducing two plist tools plb and plx

Elevator pitch

I’m excited to introduce two plist tools for macOS plb the plist broker and plx the plist extractor. The value of plb is its ability to visualize a plist as you will see below, just like my tool jpt does for JSON. plb can map out the complete paths and values that may be buried within nested dictionaries and arrays. It can help you see the “shape” of a plist and recognize patterns. They are both shell scripts and functions you can incorporate into your own scripts, like my other embeddable tools: jpt, ljt, jse and shui

Because Reasons, part 1

It was while writing Detecting Apple Intelligence and ChatGPT Integration Status last November, I decided to make a plist tool that could thoroughly map and navigate plist paths, easily extract plain text values and most importantly, effortlessly navigate into plists stored as base64 encoded data. What I thought was a new trend, is actually not: Binary plist data in plists is nothing new and if that’s the case then, it’s about time we have a tool for work with them! Funnily enough, in the above post I complained with an “ATTN: Craig Federighi” about the obfuscation games I felt Apple engineers were playing with the Apple Intelligence prefs as these inscrutable data blobs. Now, in the upcoming macOS 15.4 release, lo and behold those keys have disappeared from .GlobalPreferences! πŸ’₯ Um, thanks, I guess? “I see dead keys” now. 😬 “Be careful what you wish for, it might diminish your tool’s relevance!” as they (kinda) say. Still, there’s lots of binary plist data embedded in your preferences, when you run the examples for plb below you’ll see.

Because Reasons, part 2

You might still be asking yourself: “Why do we need yet another plist tool though? We have defaults, PlistBuddy, and plutil. Why do we need anything else?!”. Well, let me tell you they all have their strengths and weaknesses:

  • defaults can only get the value of a top level key, it does not work with nested paths; it cannot output XML unless it is the entire plist; it converts ISO 8601 dates to local time using abbreviated time zone which are notoriously hard to parse with date; empty preferences return an error as if they don’t exist; it cannot take piped input; binary data is output as an abridged hex dump
  • With plutil -extract there is no way to extract data from the root of a non-dictionary type plist, a key name is always required; key paths are period delimited, so keys with periods in their name require \\ escapes which gets unsightly quickly with when reverse domain style key names are used; the JSON extract method balks at outputting most plist data types (real, boolean, date, data); the -convert xml1 function makes it too easy to overwrite the source file if you forget to add the awkward -o - syntax to send to stdout versus the source file
  • PlistBuddy is a mixed bag: it can work with paths; it has a colon delimited key path notation that’s not flummoxed by periods; colons are more easily escaped with a single vs. double slash; it can output XML with -x; however, it cannot process binary plist data; it adds a newline to all output which alters binary output and especially corrupts binary plist data; redirected input requires /dev/stdin to be specified as the file; piped input does not work, only here documents do (awkward looking one-liners ensue)

plb in action

Getting the lay of the land

One of the most powerful features came about quite late in development: “crawling”. That is, plb can iterate over a plist outputting paths, types and values (depending on the specified depth) and even traverse into plist data stored as data. The go-to exploratory command is: plb -V (which is shorthand for -CCC, the 3rd level of crawl output):

Path >>> type >>> value

Every simple type, except data, can be seen above: string, bool, integer, real and date. As well as the “complex” types of array and dict. An array’s indices are enumerated and a dictionary will have all paths traced with colons delimiting the path of additional dictionaries and arrays within. Let’s see what happens when we hit some data type nodes:

Each indented section is a decoded binary blob of plist data

As you can see the -V will cause plb to go into property list data within data types, a lowercase -v will not traverse into plists encoded as data (same with upper/lowercase crawl option -c/-cc and -C/-CC). When this “traversal” occurs, the path is indented for easier visual identification and the pipe character | is used to delimit. In the above pic we can see both empty and populated arrays. There are some preferences like com.apple.ncplugin.weather that have two levels of nesting!

Is this the “Inception” of plists?πŸͺ†

Lastly, here’s what binary data looks like in -v/-V mode. The file type is inside parentheses after data and on the next line(s) are the binary data rendered with xxd

More work with embedded plist data

Now that we’ve checked out the “crawling” output, let’s take a look at the key that inspired all this work com.apple.gms.availability.key. You probably won’t find this key on new 15.4 installs but it might hang around if you upgraded from 15.0 to 15.3. In this example, if you ran defaults export .GlobalPreferences - you’d only see the key as a base64 encoded data string and you could get just that key in XML. If you specify the key defaults gives you an abbreviated (and useless) hex dump.

I don’t speak robot, do you? πŸ€–

If you use plutil to extract the value of the key, it’d not only require some tedious escaping of the periods but would still only get you the base64 encoded string which would require piping into another plutil command to convert to xml1. Not to mention the -extract syntax always throws me off!

Two more ways to get nothing particularly useful
Does the “juice look worth the squeeze” in either of these options?

PlistBuddy can kinda output the raw binary plist except it appends a newline to everything, which corrupts the binary plist data and plutil fails. perl can chomp it off or in zsh you can capture the output using command substitution $() since zsh can handle nulls in variables this will strip the extraneous newline but again, you’d still have to convert or extract the value plutil! Instead, let’s check out how easy it is with plb. Let’s again start with the encoded XML output: plb -x .GlobalPreferences com.apple.gms.availability.key

This again but much easier to get to.

-x ensures XML output and it also keeps the data from being automatically decoded and output as plaintext. How about we “traverse” into the embedded plist data and output it as XML. We simply append a pipe character | to the end of the path, like we saw in the “crawled” paths above. Make sure to either escape or quote the pipe character in the path argument like this (so the shell does not misunderstand): plb -x .GlobalPreferences 'com.apple.gms.availability.key|:’ (the root colon is optional, fyi)

Wow, this is a lot easier already! You can also crawl the key itself with -V and get simple overview

Since the array has only a simple integer value, it will easily output as plain text with this command: plb .GlobalPreferences com.apple.gms.availability.key

plb: Keeping it simple (so-and-so)

Look at that, would you! A simple number from a simple command, neato! BTW: While this key might be going the way of the dodo, the value in macOS 15.0-15.3 indicates (in a roundabout way) that Apple Intelligence is off (2) or on (0) – 1 I think means it’s downloading and installing. I built plb to attempt to give you plaintext output by default. Only a dict or data key will cause output to be XML, otherwise all “simple” types are be output as plain text.

What’s plx for then?

Glad you asked! Despite making plb.min.sh a one-liner minified version, it’s still pretty big at 18k (but down from 36k for the fully commented version!). I can understand if you are embedding that function in your script, you might want something smaller.

plx has the essential feature that plb does: It can dive into nested data plists using the same colon delimited key paths and pipe characters that plb does, it just omits some of the fancy output that plb does but it has the most essential functions (it’s debatable they too could be removed but the functions were either small or necessary for other internal processes):

  • -l for getting key names within a dictionary or the length of an array
  • -t for getting the plist type of a node
  • -e to convert ISO 8601 dates to epoch

This gets plx.min.sh down below 10k! You might feel better about embedding in your script. Your call. Same easy syntax:

plx: All the output, half the bytes (embedded in your script)

Wrapping Up

Check them both out at the plb GitHub repo and the plx GitHub repo. They both have a full sized and commented shell script as well as a one-liner/minified version. In their Releases (plb, plx) you’ll find a macOS package (pkg) download that will install the scripts to a self-named folder in /usr/local/ and then create a symlink to the main script without the .sh extension in /usr/local/bin so you can run it with ease! I hope you get some use out of these.

Homework

Make sure to check out the Advanced Examples in the plb -h help output (also included down below) for some simple loops that can crawl through all your user prefs, you might be surprised at what you find!

#plb Advanced Examples
#zsh only, this ignores comment lines if you copy/paste the examples
 set -k

#add to this comma separated list of large preference domains to skip
skipDomains="com.apple.audio.AudioComponentCache,com.apple.SpeakSelection,com.apple.garageband10,com.apple.EmojiCache,com.apple.audio.InfoHelper"

#1 Get key paths and values for all user preferences 
# substitute `plb -D` for ByHost user prefs
IFS=$'\n'; for domain in $(plb -d); do
 #skip large domains
 grep -qE '(^|,)'"${domain}"'(,|$)' <<< "${skipDomains}" && continue
 echo "## Domain: $domain"; echo "## File Path: $(plb -f "${domain}")"; plb -V "${domain}"; echo
done

#2 Get key paths and values for all .plist files in /Library/Preferences
IFS=$'\n'; for domain in $(find /Library/Preferences -type f -name '*plist'); do
 grep -qE "(^|,)${domain}(,|$)" <<< "${skipDomains}" && continue
 echo "## Domain: $domain"; echo "## File Path: $(plb -f "${domain}")"
 # Adjust the info level/output below
 plb -V "${domain}"; echo
done

# Examples 1 & 2 exercises
 # Adjust the info level/output from -V to -CC or -C then try shallower crawls with -cc and -c
 # Output XML with `plb -x`, JSON with `plb -j`; both are speedier than crawling but with tradeoffs
 # `plb -p` prints quickly with PlistBuddy, while key/value pairs are easily spotted, paths are not

#3 Get the `file` type of all data nodes in your user prefs, you might find something interesting
IFS=$'\n'; for domain in $(plb -d); do
 grep -qE "(^|,)${domain}(,|$)" <<< "${skipDomains}" && continue
 echo "## Domain: $domain"
 echo "## Path: $(plb -f "${domain}")"
 #the sed at the end is if you use -CC, its removes the indents from the paths of data embedded plist
 for datapath in $(plb -cc "${domain}" | awk -F ' >>> ' '/data$/ {print $1}' | sed 's/^ *//'); do 
  #get file type of data and key path
  echo "$(plb -F "$domain" "$datapath") <<< ${datapath}"
 done
 echo
done

# Example 3 notes: 
 # `plb -cc` does not go into data plists, use `plb -CC` to search within data plists
 # Use the awk field separator `-F ' >>> '` to easily parse -cc/-CC output ($1 paths, $2 type)

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.

#!/bin/bash
#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)
icon="${6}"
#invoke the system open command with this argument (URL, preference pane, etc...)
open_item="${7}"

#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";;
esac

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

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
activate
with timeout of 86400 seconds
	display dialog "${message}" with title "${title}" ${withIcon_AS} buttons {"OK"} default button "OK" giving up after "86400"
end timeout
EOF

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

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!
> EOF
"\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!

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.