It’s been more than a month since the last update, and while there has been work on the dev branch, I was quite distracted with other things (like this). The good news is, that there have been quite a few contributions from others! A huge thanks to all who helped make this a better script.
All it took was for me to find some time to put all the contributions together, which I finally found some time for.
What’s new in v0.3:
added several new labels for total of 98
removed the powershell labels, since the installer is not notarized
when run without any arguments, the script now lists all labels
changed how zips are expanded because this was broken on Mojave
Since then, it has gotten lots of feedback from others and many contributions. As the changes, fixes and additional apps have accumulated, I have created a 0.2 release to get a stable new version. If you like living on the edge you can also use the dev branch for the latest update.
Changes in this version:
many fixes for broken URLs and other bugs
pkgInDmg and pkgInZip now search for the first pkg file in the archive in case the file name varies with the version
notification on successful installation can be suppressed with the NOTIFY variable
Apple signed installers and apps that don’t have a Team ID are verified correctly now
several new applications: count increased from 62 in v0.1 to 87 in v0.2
Quoting strings and variable substitutions is a bit of a dark art in shell scripts. It looks simple and straightforward enough, but there are lots of small devils in the details, that can come out and haunt you.
Basics: why we quote strings
In shell scripts (sh, bash, and zsh) you use the equals character = to assign a string value to a variable:
As long as there are no special characters in the literal string, there is no need to quote the string.
When you use the variable, you prefix a $ symbol:
> echo $name
> cd $dirpath
When the literal string contains special characters, you need to either escape the special characters with the backslash \ or quote the entire string with either single quotes ' or double quotes ". Space is proverbial ‘killer character’, especially for file paths. (More details in this post.)
The difference between single quotes and double quotes is important. Single quotes escape every special character except the single quote itself. A single quoted string of '#$"\!' will represent exactly those characters.
Double quotes escape most characters, except the double quote " the backtick `, the dollar sign $, the backslash \, and the exclamation mark !. (There are slight differences between the shells on this.)
This allows us to use old-style command substitution with backticks and variable substitution (dollar sign) within double quoted strings:
> echo "Hello, $name"
Hello, John Doe
> echo "The Computer Name is `scutil --get ComputerName`"
Though you should be using the $(…) syntax for command substitution instead of backticks `. The parenthesis syntax is more readable and can be nested.
In general, it is a good rule to always quote literal strings. Whether you should use double quotes or single quotes depends on the use case.
Combining literal strings with special characters
Things can start getting complicated when you want special characters with their special functionality. For example, when you want to refer to the path ~/Library/Application Support, you should put it in quotes, because of the space. But when you put the ~ in the quotes, it will not be substituted to the user’s home directory path.
There are a few ways to solve this problem. You could escape the space with a backslash. You could use the $HOME variable instead (but be sure you are in a context where this is set). But the easiest is to move the special character out of the quotes:
Quotes in quotes
Sometimes it is necessary to have a set of quotes within quotes. A common situation for MacAdmins is the following osascript:
osascript -e 'display dialog "Hello, World"'
The osascript command can be used to run Apple commands or scripts. Since AppleScript uses double quotes for literal strings, the entire AppleScript command is passed in single quotes. This keep the command string together and the double quotes in single quotes don’t confuse the shell.
This works fine, until you want to do something like this:
computerName=$(scutil --get ComputerName)
newName=$(osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "$computerName")')
Again, we put the AppleScript command in single quotes, so we can use double quotes inside. But now, the single quotes are also blocking the variable substitution and we get the literal $computerName in the dialog.
There are a few solutions out of this, I will demonstrate three:
First, you could close the single quotes before the variable substitution and re-open them after:
osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'$computerName'")'
This will in this form as long as $computerName contains no spaces. This is unlikely as the default computer name is something like Armin's MacBook Pro. The shell will consider that space a separator before a new argument, breaking the AppleScript command into meaningless pieces and failing the osascript command. We can avoid that by putting the substitution itself in double quotes:
osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'"$computerName"'")'
This works and is entirely legal syntax, but not very legible.
Escaping the escape characters
Another solution is to use double quotes for the entire AppleScript command, we can use variable substitution inside. But then we have to deal with the double quotes required for the AppleScript string literal. The good news here is that we can escape those with the backslash:
osascript -e "text returned of (display dialog \"Enter Computer Name\" default answer \"$computerName\")"
This doesn’t win prizes for legibility either, but I consider it an improvement over the previous approach.
The above approaches with work in sh, bash, and zsh. But bash and zsh have another tool available that can work here. The ‘here doc’ syntax can be used to include an entire block of AppleScript code in a bash or zsh script:
computerName=$(scutil --get ComputerName)
text returned of (display dialog "Enter Computer Name" default answer "$computerName")
echo "New name: $newName"
The syntax is a bit weird. The <<EndOfScript says: take all the text until the next appearance of EndOfScript and pipe it into the preceding command, in this case osascript.
The ‘marker’ EndOfScript is entirely arbitrary. Many people choose EOF but I prefer something a little more descriptive. Whatever label you choose the ending marker has to stand alone in its line. This is why the parenthesis ) which closes the command substition $( has to stand alone in the next line.
You can still use variable substitution in a here doc, so the variable $computerName will be substituted before the here doc is piped into osascript.
Apple has included a version of Python 2 with Mac OS X since 10.2 (Jaguar). Python 3.0 was released in 2008 and it was not fully backwards compatible with Python 2. For this reason, Python 2 was maintained and updated alongside Python 3 for a long time. Python 2 was finally sunset on January 1, 2020. Nevertheless, presumably because of the compatibility issues, Apple has always pre-installed Python 2 with macOS and still does so in macOS 10.15 Catalina. With the announcement of Catalina, Apple also announced that in a “future version of macOS” there will be no pre-installed Python of any version.
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. (macOS 10.15 Catalina Release Notes)
This also applies to Perl and Ruby runtimes and other libraries. I will be focussing on Python because it is used more commonly for MacAdmin tools, but most of this post will apply equally to Perl and Ruby. Just mentally replace “Python” for your preferred language.
The final recommendation is what AutoPkg and Munki are following: they are bundling their own Python runtime.
How to get Python
There is a second bullet in the Catalina release notes, though:
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)
This is great, right? Apple says there is a built-in Python 3! And it’s pre-installed? Just move all your scripts to Python 3 and you’ll be fine!
Unfortunately, not quite. The python3 binary does exist on a ‘clean’ macOS, but it is only a stub tool, that will prompt a user to download and install the Command Line Developer Tools (aka “Developer Command Line Tools” or “Command Line Tools for Xcode”). This is common for many tools that Apple considers to be of little interest to ‘normal,’ non-developer users. Another common example is git.
When you install Xcode, you will also get all the Command Line Developer Tools, including python3 and git. This is useful for developers, who may want to use Python scripts for build operation, or for individuals who just want to ‘play around’ or experiment with Python locally. For MacAdmins, it adds the extra burden of installing and maintaining either the Command Line Developer Tools or the full Xcode install.
Python Versions, a multitude of Snakes
After installing Xcode or the Command Line Developer Tools, you can check the version of python installed: (versions on macOS 10.15.3 with Xcode 11.3.1)
When you go on the download page for Python.org, you will get Python 3.8.1 (as of this writing). But, on that download page, you will also find download links for “specific versions” which include (as of this writing) versions 3.8.1, 3.7.6, 3.6.10, 3.5.9, and the deprecated 2.7.17.
The thing is, that Python isn’t merely split into two major release versions, which aren’t fully compatible with each other, but there are several minor versions of Python 3, which aren’t fully compatible with each other, but are still being maintained in parallel.
Developers (individuals, teams, and organisations) that use Python will often hold on to a specific minor (and sometimes even patch) version for a project to avoid issues and bugs that might appear when changing the run-time.
When you install the latest version of Munki, it will install a copy of the Python framework in /usr/local/munki/ and create a symbolic link to that python binary at /usr/local/munki/python. You can check its version as well:
% /usr/local/munki/python --version
All the Python code files for Munki will have a shebang (the first line in the code file) of
This ensures that Munki code files use this particular instance of Python and no other copy of Python that may have been installed on the system.
The latest version of AutoPkg has a similar approach:
In both cases the python binary is a symbolic link. This allows the developer to change the symbolic link to point to a different Python framework. The shebangs in the all the code files point to the symbolic link, which can be changed to point to a different Python framework.
This is useful for testing and debugging. Could MacAdmins use this to point both tools to the same Python framework? Should they?
The Bridge to macOS
On top of all these different versions of Python itself, many scripts, apps, and tools written in Python rely on ‘Python modules.’ These are libraries (or frameworks) of code for a certain task, that can be downloaded and included with a Python installation to extend the functionality of Python.
The most relevant of these modules for MacAdmins is the “Python Objective-C Bridge.” This module allows Python code to access and use the native macOS Cocoa and CoreFoundation Frameworks. This not only allows for macOS native GUI applications to be written in Python (e.g. AutoDMG and Munki’s Managed Software Center [update: MSC was re-written in Swift last year]), but also allows short scripts to access system functions. This is sometimes necessary to get a data that matches what macOS applications “see” rather than what the raw unix tools see.
For example, the defaults tool can be used to read the value of property lists on disk. But those might not necessarily reflect the actual preference value an application sees, because that value might be controlled by a different plist file or configuration profile.
You could build a tool with Swift or Objective-C that uses the proper frameworks to get the “real” preference value. Or you can use Python with the Objective-C bridge:
from Foundation import CFPreferencesCopyAppValue
print CFPreferencesCopyAppValue("idleTime", "com.apple.screensaver")
Three simple lines of Python code. This will work with the pre-installed Python 2.7, because Apple also pre-installs the Python Objective-C bridge with that. When you try this with the Developer Tools python3 you get an error:
ModuleNotFoundError: No module named 'Foundation'
This is because the Developer Tools do not include the Objective-C bridge in the installation. You could easily add it with:
> sudo python3 -m pip install pyobjc
But again, while this command is “easy” enough for a single user on a single Mac, it is just the beginning of a Minoan labyrinth of management troubles.
Developers and MacAdmins, have to care about the version of the Python they install, as well as the list of modules and their versions, for each Python version.
It is as if the Medusa head kept growing more smaller snakes for every snake you cut off.
(Ok, I will ease off with Greek mythology metaphors.)
You can get a list of modules included with the AutoPkg and the Munki project with:
> /usr/local/munki/python -m pip list
> /usr/local/autopkg/python -m pip list
You will see that not only do Munki and AutoPkg include different versions of Python, but also a different list of modules. While Munki and AutoPkg share many modules, their versions might still differ.
Snake Herding Solutions
Apple’s advice in the Catalina Release Notes is good advice:
It’s recommended that you bundle the runtime within the app.
Rather than the MacAdmin managing a single version of Python and all the modules for every possible solution, each tool or application should provide its own copy of Python and its required modules.
This might seem wasteful. A full Python 3 Framework uses about 80MB of disk space, plus some extra for the modules. But it is the safest way to ensure that the tool or application gets the correct version of Python and all the modules. Anything else will quickly turn into a management nightmare.
This is the approach that Munki and AutoPkg have chosen. But what about smaller, single script solutions? For example simple Python scripts like quickpkg or prefs-tool?
Should I bundle my own Python framework with quickpkg or prefs-tool? I think that would be overkill and I am not planning to do that. I think the solution that Joseph Chilcote chose for the outset tool is a better approach for less complex Python scripts.
In this case, the project is written to run with Python 3 and generic enough to not require a specific version or extra modules. An admin who wants to use this script or tool, can change the shebang (the first line in the script) to point to either the Developer Tool python3, the python3 from the standard Python 3 installer or a custom Python version, such as the Munki python. A MacAdmin would have to ensure that the python binary in the shebang is present on the Mac when the tool runs.
You can also choose to provide your organization’s own copy Python with your chosen set of modules for all your management Python scripts and automations. You could build this with the relocatable Python tool and place it in a well-known location the clients. When updates for the Python run-time or modules are required, you can build and push them with your management system. (Thanks to Nathaniel Strauss for pointing out this needed clarifying.)
When you build such scripts and tools, it is important to document which Python versions (and module versions) you have tested the tool with.
(I still have to do that for my Python tools.)
What about /usr/bin/env python?
The env command will determine the path to the python binary in the current environment. (i.e. using the current PATH) This is useful when the script has to run in various environments where the location of the python binary is unknown.
This is useful when developers want to use the same script in different environments across different computers, user accounts, and platforms. However, this renders the actual version of python that will interpret the script completely unpredictable.
Not only is it impossible to predict which version of Python will interpret a script, but you cannot depend on any modules being installed (or their versions) either.
For MacAdmin management scripts and tools, a tighter control is necessary. You should use fixed, absolute paths in the shebang.
Managing Python runtimes might seem like a hopeless sisyphean task. I believe Apple made the right choice to not pre-install Python any more. Whatever version and pre-selection of module versions Apple would have chosen, it would only have been the correct combination for a few Python solutions and developers.
While it may seem wasteful to have a multitude of copies of the Python frameworks distributed through out the system, it is the easiest and most manageable solution to ensure that each tool or application works with the expected combination of run-time and modules.
And because you need to use a newer Xcode and Swift 5 to notarize the tool, it now requires the Swift 5 Runtime support for command line tools when you install it on versions of macOS older than 10.14.4. (On those older versions, you can also continue to use desktoppr-0.1)
The release notes for Catalina also tell us that other built-in scripting runtimes, namely Python, Perl, and Ruby. Will not be included in future macOS releases (post-Catalina) any more.
This means, that if you want to use bash, Python, Perl, or Ruby on macOS, you will have to install, and maintain your own version in the future.
However, scripts in installation packages, cannot rely on any of these interpreters being available in future, post-Catalina versions of macOS. Installer pkgs can be run in all kinds of environments and at all times, and you would not want them to fail, because a dependency is missing.
The good news is that we still have time. All the runtimes mentioned above are still present in Catalina, so the packages will continue to work for now. But if you are building installation scripts, you need to check if any of the installation scripts use one of these interpreters and fix them.
I recommend to use /bin/sh for installation scripts, since that will run in any macOS context, even the Recovery system.
If you are using third-party installer packages, you may also want to check them for these interpreters, and notify the developer that these packages will break in future versions of macOS.
To check a flat installer package, you would expand it with pkgutil --expand and then look at script files in the Scripts folder. This will work fine for a package or two, but gets tedious really quickly, especially with large distribution pkgs with many components (e.g. Office).
So… I wrote a script to do it. The script should handle normal component pkgs, distribution pkgs and the legacy bundle pkgs and mpkgs.
Once I had written the code to inspect all these types of pkgs, I realized I could grab all other kinds of information, as well. The pkgcheck.sh script will check for:
Signature and Notarization
Type of Package: Component, Distribution, legacy bundle or mpkg
Identifier and version (when present)
for Distribution and mpkg types, shows the information for all components as well
for every script in a pkg or component, checks the first line of the script for shebangs of the deprecated interpreters (/bin/bash, /usr/bin/python, /usr/bin/perl, and /usr/bin/ruby) and print a warning when found
How to run pkgcheck.sh
Run the script with the target pkg file as an argument:
% ./pkgcheck.sh sample.pkg
You can give more than one file:
% ./pkgcheck.sh file1.pkg file2.pkg ...
When you pass a directory, pkgcheck.sh will recursively search for all files or bundle directories with the pkg or mpkg extension in that directory:
% ./pkgcheck.sh SamplePkgs
Features and Errors
There are a few more things that I think might be useful to check in this script. Most of all, I want to add an indicator whether a component is enabled by default or not. If you can think of any other valuable data to display, let me know. (Issue or Pull Request or just ping me on MacAdmins Slack)
I have tested the script against many pkgs that I came across. However, there are likely edge cases that I haven’t anticipated, which might break the script. If you run into any of those, let me know. (File an Issue or Pull Request.) Having the troublesome pkg would of course be a great help.
Note: the script will create a scratch directory for temporary file extractions. The script doesn’t actually expand the entire pkg file, only the Scripts sub-archive. The scratch folder will be cleaned out at the beginning of the next run, but not when the script ends, as you might want to do some further inspections.
This is a sample pkg I build in my book, it has pre- and postinstall scripts using a /bin/bash shebang:
Still a lot of fallout from the Zoom invulnerability. It took Apple three updates to MRT (so far) to eradicate all the differently branded varieties of the Zoom client web server. Makes me wonder how many Macs there were or are with multiple of these clients installed.
We also got new betas for 10.14.5 and 10.15 and the respective iOS versions. Some of the worst data-destroying bugs seem to be fixed or at least mitigated but I am still not comfortable moving my production devices to the betas. That’s what test devices are there for. I am getting quite excited about some of the features I have seen, both for end-users and administrators.
One of those features is that zsh will become the default shell for macOS. If you want to know what that means, how to transfer your shell configuration, workflows, and scripts from bash to zsh, and increase your Terminal productivity, we are doing a half-day training in Amsterdam on September 6. You can get more details and sign up on our webpage!
Daniel Jalkut: “Apple fixed a niggling little AppleScript problem in macOS Catalina beta 4. It’s reassuring to me that they bothered to address this. Thanks!”
Jonathan Grynspan: “macOS Catalina Seed 4: Finder’s Open With menu has been rebuilt from scratch. You should never again see ”Fetching…“! Although in normal usage this should not impact you, if you do notice issues with the Open With menu, please file bug reports!”