The unexpected return of JavaScript for Automation

Monterey has deprecated the pre-installed python on macOS. To be precise, built-in python has been deprecated since macOS Catalina, but Monterey will now throw up dialogs warning the user that an app or process using built-in python needs to be updated.

I and others have written about this before:

So far, I have recommended to build native Swift command line tools to replace python calls. However, from discussions in MacAdmins Slack, a new option has emerged. Most of the credit for popularizing and explaining this goes to @Pico (@RandomApps on Twitter) in the #bash and #scripting channels.

(Re-)Introducing JavaScript for Automation

AppleScript has been part of macOS since System 7.1. In the late nineties, there was concern that it wouldn’t make the transition to Mac OS X, but AppleScript made the jump and has happily co-existed with the Terminal and shell scripting as an automation tool on macOS. AppleScript has a very distinct set of strengths (interapplication communication) and weaknesses (awkward syntax and inconsitent application functionality and dictionaries) but it has been serving its purpose well for many users.

With Mac OS X 10.4 Tiger, Apple introduced Automator, which provided a neat UI to put together workflows. Much of Automator was based on AppleScript and users expected a more and improved AppleScript support because of that going forward. Instead, we saw AppleScript’s support from Apple and third parties slowly wane over the years.

AppleScript is stil very much present and functional in recent versions of macOS. It just seems like it hasn’t gotten much love over the last decade or so. Now that Shortcuts has made the jump from iOS, there may be hope for another revival?

The last major changes to AppleScript came with Mavericks and Yosemite. Mavericks (10.9) included a JavaScript syntax for the Open Scripting Architecture (OSA), which is the underlying framework for all AppleScript functionality. Apple called this “JavaScript for Automation.” Because this is a mouthful, it often abbreviated as JXA.

The JavaScript syntax and structure is more like a “real” programming language, than the “english language like” AppleScript. Once again this raised hopes that this could attract more scripters to AppleScript and thus encourage Apple and third party developers to support more AppleScript. But unfortunately, this positive re-inforcement did not take off.

Then Yosemite (10.10) made the AppleScript-Objective-C bridge available everywhere in AppleScript. Previously, the Objective-C bridge was only available when you built AppleScript GUI applications using AppleScript Studio in Xcode. The Objective-C bridge allows scripters to access most of the functionality of the system frameworks using AppleScript or JXA.

The coincidence of these two new features might be the reason that the ObjC bridge works much better using JXA than it does with the native AppleScript syntax.

JXA and Python

What does JXA and the AppleScriptObjC bridge have to do with the Python deprecation in modern macOS?

One reason python became so popular with MacAdmins, was that the pre-installed python on Mac OS X, also came with PyObjC, the Objective-C bridge for python. This allowed python to build applications with a native Cocoa UI, such as AutoDMG and Munki’s Managed Software Center. It also allowed for short python scripts or even one-liners to access system functionality that was otherwise unavailable to shell scripts.

For example, to determine if a preference setting in macOS is enforced with a configuration profile, you can use CFPreferences or NSUserDefaults.

Objective-C/C:

BOOL isManaged =CFPreferencesAppValueIsForced("idleTime", "com.apple.screensaver")

Swift:

let isManaged = CFPreferencesAppValueIsForced("idleTime", "com.apple.screensaver")

The Objective-C bridge allows to use this call from python, as well:

from Foundation import CFPreferencesAppValueIsForced
isManaged=CFPreferencesAppValueIsForced("idleTime", "com.apple.screensaver")

With JXA and the AppleScriptObjC bridge, this will look like this:

ObjC.import('Foundation');
$.CFPreferencesAppValueIsForced(ObjC.wrap('idleTime'), ObjC.wrap('com.apple.screensaver'))

Now, this looks really simple, but working with any Objective-C bridge is always fraught with strange behaviors, inconsistencies and errors and the JXA ObjC implementation is no different.

For example, I wanted to change the code above to return the value of the setting instead of whether it is managed. The CFPreferences function for that is called CFPreferencesCopyAppValue and it works fine in Swift and Python, but using JXA it only ever returned [object Ref]. The easiest solution was to switch from the CFPreferences functions to using the NSUserDefaults object:

ObjC.import('Foundation');
ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('$1').objectForKey('$2'))

(Once again many thanks to @Pico on the MacAdmins Slack for helping me and everyone else with this and also pointing out, that there is a different, somewhat complicated, solution to the object Ref problem. I will keep that one bookmarked for situations where there is no alternative Cocoa object.)

We used this to remove the python dependency from Mischa van der Bent’s CIS-Scripts.

JXA in shell scripts

To call JXA from a shell script, you use the same osascript command as for normal AppleScript, but add the -l option option to switch the language to JavaScript:

osascript -l JavaScript << EndOfScript
     ObjC.import('Foundation');
    ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('idleTime').objectForKey('com.apple.screensaver'))
EndOfScript

For convenience, you can wrap calls like this in a shell function:

function getPrefValue() { # $1: domain, $2: key
      osascript -l JavaScript << EndOfScript
     ObjC.import('Foundation');
    ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('$1').objectForKey('$2'))
EndOfScript
}

function getPrefIsManaged() { # $1: domain, $2: key
     osascript -l JavaScript << EndOfScript
     ObjC.import('Foundation')
     $.CFPreferencesAppValueIsForced(ObjC.wrap('$1'), ObjC.wrap('$2'))
EndOfScript
}

echo $(getPrefValue "com.apple.screensaver" "idleTime")
# -> actual value
echo $(getPrefIsManaged "com.apple.screensaver" "idleTime")
# -> true/false

Note that the $ character does a lot of work here. It does the shell variable substitution for the function arguments in the case of $1 and $2. These are substituted before the here doc is piped into the osascript command. The $. at the beginning of the command is a shortcut where $ stands in for the current application and serves as a root for all ObjC objects.

There is also a $(…) function in JXA which is short for ObjC.unwrap(…) but I would recommend against using that in combination with shell scripts as shell’s command substitution has the same syntax and would happen before the JavaScript is piped into osascript.

There is a GitHub wiki with more detailed documentation on using JXA, and the JXA Objective-C bridge in particular.

JXA for management tasks

I’ll be honest here and admit that working with JXA seems strange, inconsistent, and — in weird way — like a step backwards. Putting together a Command Line Tool written in Swift feels like a much more solid (for lack of a better word) way of solving a problem.

However, the Swift binary command line tool has one huge downside: you have to install the binary on the client before you can use it in scripts and your management system. Now, as MacAdmins, we usually have all the tools and workflows available to install and manage software on the client. That’s what we do.

On the other hand, I have encountered three situations (set default browser, get free disk space, determine if a preference is managed) where I needed to replace some python code in the last few months and I would have no trouble finding a few more if I thought about it. Building, maintaining, and deploying a Swift CLI tool for each of these small tasks would add up to a lot of extra effort, both for me as the developer and any MacAdmin who wants to use the tools.

Alternatively, you can deploy and use a Python 3 runtime with PyObjC, like the MacAdmins Python and continue to use python scripts. That is a valid solution, especially when you use other tools built in python, like Outset or docklib. But it still adds a dependency that you have to install and maintain.

In addition to being extra work, it adds some burden to sharing your solutions with other MacAdmins. You can’t just simply say “here’s a script I use,” but you have to add “it depends on this runtime or tool, which you also have to install.

Dependencies add friction.

This is where JXA has an advantage. Since AppleScript and its Objective-C bridge are present on every Mac (and have been since 2014 when 10.10 was released) there is no extra tool to install and manage. You can “just share” scripts you build this way, and they will work on any Mac.

For example, I recently built a Swift command line tool to determine the free disk space. You can download the pkg, upload it to your management system, deploy it on your clients and then use a script or extension attribute or fact or something like to report this value to your management system. Since there is a possibility that the command line tool is not yet installed when the script runs, you need to add some code to check for that. All-in-all, nothing here is terribly difficult or even a lot of work, but it adds up.

Instead you can use this script (sample code for a Jamf extension attribute):

#!/bin/sh

freespace=$(/usr/bin/osascript -l JavaScript << EndOfScript
    ObjC.import('Foundation')
    var freeSpaceBytesRef=Ref()
    $.NSURL.fileURLWithPath('/').getResourceValueForKeyError(freeSpaceBytesRef, 'NSURLVolumeAvailableCapacityForImportantUsageKey', null)
    ObjC.unwrap(freeSpaceBytesRef[0])
EndOfScript
)

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

Just take this and copy/paste it in the field for a Jamf Extension Attribute script and you will get the same same free disk space value as the Finder does. If you are running a different management solution, it shouldn’t be too difficult to adapt this script to work there.

The Swift tool is nice. Once it is deployed, there are some use cases where it could be useful to have a CLI tool available. But most of the time, the JXA code snippet will “do the job” with much less effort.

Note on Swift scripts

Some people will interject with “but you can write scripts with a swift shebang!” And they are correct. However, scripts with a swift shebang will not run on any Mac. They will only run with Xcode, or at least the Developer Command Line Tools, installed. And yes, I understand this is hard for developers to wrap their brains around, but most people don’t have or need Xcode installed.

When neither of these are installed yet, and your management system attempts to run a script with a swift shebang, it will prompt the user to install the Developer command line tools. This is obviously not a good user experience for a managed deployment.

As dependencies go, Xcode is a fairly gigantic installation. The Developer Command Line Tools much less so, but we are back in the realm of “install and manage a dependency.”

Parsing JSON

Another area where JXA is (not surprisingly) extremely useful is JSON parsing. There are no built-in tools in macOS for this so MacAdmins either have to install jq or scout or fall back to parsing the text with sed or awk. Since JSON is native JavaScript, JXA “just works” with it.

For example the new networkQuality command line tool in Monterey has a -c option which returns JSON data instead of printing a table to the screen. In a shell script, we can capture the JSON in a variable and substitute it into a JXA script:

#!/bin/sh

json=$(networkQuality -c)

osascript -l JavaScript << EndOfScript
    var result=$json
    console.log("Download:  " + result.dl_throughput)
    console.log("Upload:    " + result.ul_throughput)
EndOfScript

Update: (2021-11-24) Paul Galow points out that this syntax might allow someone to inject code into my JavaScript. This would be especially problematic with MacAdmin scripts as those often run with root privileges. The way to avoid this injection is too parse the JSON data with JSON.parse :

#!/bin/sh 

json=$(networkQuality -c) 

osascript -l JavaScript << EndOfScript     
  var result=JSON.parse(\`$json\`)     
  console.log("Download:  " + result.dl_throughput)     
  console.log("Upload:    " + result.ul_throughput) 
EndOfScript

(I am leaving the original code up there for comparison.)

Conclusion

After being overlooked for years, JXA now became noticeable again as a useful tool to replace python in MacAdmin scripts, without adding new dependencies. The syntax and implementation is inconsistent, buggy, and frustrating, but the same can be said about the PyObjC bridge, we are just used it. The community knowledge around the PyObjC bridge and solutions goes deeper.

However, as flawed as it is, JXA can be a simple replacement for the classic python “one-liners” to get data out of a macOS system framework. Other interesting use cases are being discovered, such as JSON parsing. As such, JavaScript for Automation or JXA should be part of a MacAdmins tool chest.

Monterey, python, and free disk space

With Montery, many MacAdmins have been seeing dialogs that state:

“ProcessName” needs to be updated

and often the “ProcessName” is your management system. As others have already pointed out, the process, or scripts this process is calling, is using the pre-installed Python 2.7 at /usr/bin/python.

This is Apple’s next level of warning us that that the pre-installed Python (and Perl and Ruby) is deprecated and going away in “future version of macOS.” I have written about this before.

Even though the management system will be identified as the process that “needs to be updated,” the culprits are scripts and scriptlets that the management system calls for for management tasks (e.g. policies, tasks, scripts) and information gathering (e.g. extension attributes, facts, etc.). Ben Tom’s post above has information on how to identify scripts which may use python in a Jamf Pro server.

You can suppress the warning using a configuration profile. While this a useful measure to avoid confusing users with scary dialogs, you will have to start identifying and fixing scripts that are written entirely in python or just use simple python calls, and replacing them with non-python solutions.

Python 2.7 is not getting any more security patches and I assume Apple is eager to remove it from macOS. The clock is really ticking on this one.

Current User

The most common python call is probably the one which determines the currently logged in user. The python call for this was developed by Mike Lynn and popularized by Ben Toms in this post and has been a reliable MacAdmin tool for years. I have written about this and introduced a shell-based solution discovered by Erik Berglund.

But there are other use cases, where it is not so straight forward to replace the python code. The built-in python is so popular for MacAdmin tasks because it comes with PyObjC which allows access to the macOS system frameworks. With a few python calls you can avoid having to build an Objective-C or Swift command line tool.

Desktop Picture

I built desktoppr for this reason. The standard way to set a desktop picture with locking it down was a line of AppleScript. But, starting in macOS Mojave, sending AppleEvents to another process (in this case Finder) required a PPPC profile. You can also set the desktop picture using a framework call. There were python scripts out there, but the Swift solution will survive them…

Available Disk Space

Yesterday, I came across another such problem. With the recent versions of macOS, getting a value of the available disk space is not as strightforward as it used to be. There are a lot of files and data on the system, which will be cleared out when some process requires more disk space. Most of this is cache data or data that can be restored from cloud storage. But this ‘flexible’ available disk space will not be reported by the traditional tools, such as df or diskutil. The available disk space these tools report will be woefully low.

The available disk space which Finder reports will usually be much higher. There is functionality in the macOS system frameworks where apps can get the values for available that takes the ‘flexible’ files into account. There is even useful sample code!

Starting with this sample code, I built a command line tool that reports the different levels of ‘available’ disk space. When you run diskspace it will list them all. There are raw and ‘human-readable’ formats.

> diskspace                  
Available:      70621810688
Important:      231802051028
Opportunistic:  214051607271
Total:          494384795648
> diskspace -H              
Available:      70.62 GB
Important:      231.8 GB
Opportunistic:  214.05 GB
Total:          494.38 GB

The ‘Available’ value matches the actually unused disk space that df and diskutil will report. The ‘Important’ value matches what Finder will report as available. The ‘Opportunistic’ value is somewhat lower, and from Apple’s documentation on the developer page, that seems to be what we should use for automated background tasks.

For use in scripts, you can get each raw number with some extra flags:

> diskspace --available               
70628638720
> diskspace --important
231808547284
> diskspace --opportunistic
214057661159
> diskspace --total
494384795648

You can get more detail by running diskspace --help.

In Scripts

If you wanted to check if there is enough space to run the macOS Monterey upgrade (26 GB) you could do something like this:

if [[ $(/usr/local/bin/diskspace --opportunistic ) -gt 26000000000 ]]; then
     echo "go ahead"
else
    echo "not enough free disk space"
fi

Jamf Extension Attributes

Or, you can use diskspace in a Jamf Extension Attribute:

#!/bin/sh

diskspace="/usr/local/bin/diskspace"

# test if diskspace is installed
if [ ! -x "$diskspace" ]; then
    # return a negative value as error
    echo "<result>-1</result>"
fi

echo "<result>$($diskspace --opportunistic)</result>"

Since, this extension attribute relies on the diskspace tool being installed, you should have a ‘sanity check’ to see that the tool is there.

Get and install the tool

You can get the tool from the GitHub repo and I have created a (signed and notarized) installer pkg that will drop the tool in /usr/local/bin/diskspace.

Download Full Installer update

The small tool to download the InstallAssistant.pkg I built a while back, has been working fine, even on Monterey. However, earlier this week some people started noticing that it would not show the 11.6.1 installer. The reason had me stumped, and I was putting together a minor update with UI fixes and an icon for Monterey installers, when I realized what the problem was and was able to fix it. So you will actually find _two_ new releases on the GitHub repo today, but of course, you only need the latest, v1.1.1 aka “The real Monterey Update”.

Installomator update: v0.7

We have published an update for Installomator. It is now at version 0.7 and has over 340 labels!

Here are the changes in detail:

  • default for BLOCKING_PROCESS_ACTIONis now BLOCKING_PROCESS_ACTION=tell_user and not prompt_user. It will demand the user to quit the app to get it updated, and not present any option to skip it. In considering various use cases in different MDM solutions this is the best option going forward. Users usually choose to update, and is most often not bothered much with this information. If it’s absoultely a bad time, then they can move the dialog box to the side, and click it when ready.
  • script is now assembled from fragments. This helps avoid merging conflicts on git and allows the core team to work on the script logic while also accepting new labels. See the “Assemble Script ReadMe” for details.
  • We now detect App Store installed apps, and we do not replace them automatically. An example is Slack that will loose all settings if it is suddenly changed from App Store version to the “web” version (they differ in the handling of settings files). If INSTALL=force then we will replace the App Store app. We log all this.
  • Change in finding installed apps. We now look in /Applications and /Applications/Utilities first. If not found there, we use spotligt to find it. (We discovered a problem when a user has Parallels Windows installed with Microsoft Edge in it. Then Installomator wanted to update the app all the time, becaus spotlight found that Windows version of the app that Parallels created.)
  • Added bunch of new labels, and improved others.
  • Renamed buildCaseStatement.sh to buildLabel.sh and improved it a lot. It is a great start when figuring out how to create a new label for an app, or a piece of software. Look at the tutorials in our wiki.
  • Mosyle changed their app name from Business to Self-Service

I have explained the changes to building the script in the beta release post and in the readme document on the repository. If you want to build your own labels, this is very important, be sure to read that first.

Suspicious Package 4.0 Update

I recently posted about some Suspicious Package Power User Features, which was a follow-up to my MacDevOps YVR presentation “The Encyclopedia of Packages.”

As a follow-up to that follow-up, Suspicious Package was updated to version 4.0 today. (Yesterday? Time zones are strange.) The update to brings compatibility with macOS Monterey and some really nice refinements to these power user features.

First and foremost, Suspicious Package will now show the kind of package, or “package format” in the Package Info tab. This makes me very happy, not just because the FAQ references my presentation. As the FAQ correctly states, most users of packages or even the Suspicious Package application will not care much about the differences between the package formats, but for system administrators, this can determine the difference between a functional deployment or a broken workflow.

The previously ‘secret’ option to show the PackageInfo xml file is now also exposed in the preferences window, next to the option to show the Distribution XML.

It is now also easier to search for the contents of a particular component in a distribution package.

You can download the latest version of Suspicious Package and get the update notes here.

Many thanks to Randy Saldinger of Mothers Ruin Software for providing this amazing tool and further refining it!

Randy was also recently a guest on the MacAdmins Podcast. You check it out if you have not yet listened to that episode.

Installomator v0.7b1 – Prerelease

We have posted a new version of Installomator. This one brings with it major changes in how we assemble the actual script. Since this is such a big change, we decided to do a beta release first.

The changes in detail:

  • script is now assembled from fragments. This helps avoid merging conflicts on git and allows the core team to work on the script logic while also accepting new labels. See the “Assemble Script ReadMe” for details.
  • Change in finding installed apps. We now look in /Applications and /Applications/Utilities first. If not found there, we use spotligt to find it. (We discovered a problem when a user has Parallels Windows installed with Microsoft Edge in it. Then Installomator wanted to update the app all the time, becaus spotligt found that Windows version of the app that Parallels created.)
  • Added bunch of new labels
  • Improved buildCaseStatement.sh a lot. It is a great start when figuring out how to create a new label for an app, or a piece of software.
  • Mosyle changed their app name from Business to Self-Service

Why the changes?

Since the Installomator.sh script has grown to over 3000 lines, its management on git has become very unwieldy. The single file with all the logic and the data required to download and install the applications creates constant merge conflicts which add to the workload of the repo admins, especially when part of the team is working on the logic of the script while we still get PRs to add labels.

Because of that we have split the main script into multiple files which are easier to manage. Having multiple files results in less merge conflicts.

What changes when I use the script?

Nothing. When you just use the Installomator.sh, you still copy its contents from the Installomator.sh script at the root of the repository into your management service (don’t forget to change the DEBUG value). Or you install the script to the clients using the installer pkg from the Releases.

The changes will only affect you when you want to build your own application labels, modify existing labels or other wise modify the script.

How do I build my own labels now?

This is where you need to learn about the new system. To reduce merge conflicts, we have broken the big Installomator.sh script into smaller pieces. There is a utility script that can assemble the script from the pieces and even run it right away fro testing. You can get the details in the “Assemble script ReadMe”

We hope that these changes will make it easier for the Installomator team and other contributors to keep growing and improving the script.

Suspicious Package Power User Features

As many MacAdmins, I work a lot with installer packages. You can say I wrote the book about it. When you get an installer package from some vendor website, you will want to inspect it before you install it anywhere, let alone deploy it to dozens, hundreds, or even thousands of Macs in your fleet.

You can use the pkgutil tool to do this in the command line, but there are package inspector tools with a graphical interface that are very useful and popular.

One of these tools is Suspicious Package from Mothers Ruin Software. It displays all of the content and resources of a pkg file in a very nice user interface. Many people love Suspicious Package from Mothers Ruin Software. I have always had reservations about Suspicious Package, though, because I thought there were a few missing features.

Update (August 2021): The app has gotten a new major update (4.0) which makes these “power user features” even more accessible!

The missing features were in connection with distinguishing “normal” or “component” packages (which have a single payload) from “distribution” or “meta” packages (which don’t have a payload of their own, but contain one or more component packages).

I have explained the differences in a bit more detail in my MacDevOps YVR presentation “The Encyclopedia of Packages” where I (once again, ignorantly) stated that you can’t really tell them apart in the UI of Suspicious Package.

After that presentation, Mat X, one of the organizers of the conference got me in touch with the developer of Suspicious Package, Randy Saldinger, who graciously and patiently demonstrated that I was wrong.

In my defense, you really cannot tell normal packages from distribution packages in the default configuration of Suspicious Package, but if I had bothered to read the manual and/or explore the Preferences window, I would have found this option:

This will show the Distribution xml file at the top of the list of the ‘All Scripts’ pane for distribution packages. When you see no Distribution file there, the package is a component package.

The second checkmark in that preference window is also very useful. With “Component package and bundle info” enabled you can see which component contains the selected file in the info pane:

You can also search in the “All Files” tab with command-F and use the component package ID as a search criteria.

All of this is already well enough to remove the reservations I have had on Suspicious Package. But Randy shared another preference with me which puts it over the top. It is not exposed in the UI (yet) but when you run:

% defaults write com.mothersruin.SuspiciousPackage ShowRawPackageInfo -bool YES

in Terminal and re-launch Suspicious Package, you will see the raw PackageInfo xml in the “All Scripts” tab. Together with showing the Distribution xml, this allows you to inspect all the raw metadata that can be inside a pkg file.

I have also learned that you can use the search functionality in the “Help” menu of Suspicious Package which will link directly to the online documentation. Not many apps leverage this functionality, so we often forget to check for it. Kudos to Suspicious Package for using this.

Many thanks to Randy for all his work and help and for providing an excellent tool! I am very much looking forward to the next version.

Installomator v0.6

We have posted an update for Installomator, which brings it to v0.6.

The changes are as follows:

  • several new and updated labels, for a total of 302
  • versionKey variable can be used to choose which Info.plist key to get the version from
  • an appCustomVersion() {} function can now be used in a label
  • with INSTALL=force, the script will not be using updateTool, but will reinstall instead
  • added quit and quit_kill options to NOTIFY
  • updated buildCaseStatement.sh
  • updated buildInstallomatorPkg.sh to use notarytool (requires Xcode 13)
  • several minor fixes

There have been some other organizational changes as well. We have moved the repo to its own team on GitHub: Installomator/Installomator. This should reflect that I am no longer the sole, or even the main contributor. Many thanks to Søren Theilgaard, Isaac Ordonez, and Adam Codega for helping maintain this!

And many thanks to everyone else who contributed!

Installomator Updated: v0.5

It has been a while, mainly because I was busy with other things, but there finally is a new release version of Installomator!

The reason work has progressed—quite significantly—even though I was distracted is that Søren Theilgaard and Isaac Ordonez have joined the project as conributors. All of the work from 0.4 to 0.5 was from one of them. We ahve some great plans to move this tool forward, as well.

Many of these new app labels have been provided from others, either through GitHub issues, pull requests, or through comments in the #installomator channel on MacAdmins Slack. Thanks to all who contributed.

What’s new in v0.5:

  • Major update and now with help from @Theile and @Isaac
  • Added additional BLOCKING_PROCESS_ACTION handlings
  • Added additional NOTIFY=all. Usuful if used in Self Service, as the user will be notified before download, before install as well as when it is done.
  • Added variable LOGO for icons in dialogs, use LOGO=appstore (or jamf or mosyleb or mosylem or addigy). It’s also possible to set it to a direct path to a specific icon. Default is appstore.
  • Added variable INSTALL that can be set to INSTALL=force if software needs to be installed even though latest version is already installed (it will be a reinstall).
  • Version control now included. The variable appNewVersion in a label can be used to tell what the latest version from the web is. If this is not given, version checking is done after download.
  • For a label that only installs a pkg without an app in it, a variable packageID can be used for version checking.
  • Labels now sorted alphabetically, except for the Microsoft ones (that are at the end of the list). A bunch of new labels added, and lots of them have either been changed or improved (with appNewVersion og packageID).
  • If an app is asked to be closed down, it will now be opened again after the update.
  • If your MDM cannot call a script with parameters, the label can be set in the top of the script.
  • If your MDM is not Jamf Pro, and you need the script to be installed locally on your managed machines, then take a look at Theiles fork. This fork can be called from the MDM using a small script.
  • Script buildCaseStatement.sh to help with creating labels have been improved.
  • Fixed a bug in a variable name that prevented updateTool to be used
  • added type variable for value "updateronly" if the label should only run an updater tool.

And if you are counting, there are now more than 260 application labels in Installomator. However, that number is a bit inflated, because several vendors have multiple downloads for Intel and Apple Silicon apps.

Get the script and find the instructions on the GitHub repo.

If you have any feedback or questions, please join us in the #installomator channel on MacAdmins Slack.

Thanks again to all those who contributed!

(Installomator Icon credit: Mischa van der Bent)

Get Password from Keychain in Shell Scripts

MacAdmin scripts often require passwords, mostly for interactions with APIs.

It is easiest to store the password in clear text, but that is obviously a terrible solution from a security perspective. You can pass the password as an argument to your script, but that is inconvenient and may still appear in clear text in the ps output or the shell history.

You can obfuscate the password with base64, but that is easily reversible. You can even try to encrypt the password, but since the script needs to be able to decrypt the password, you are just adding a layer of complexity to the problem.

macOS has a keychain, where the user can store passwords and allow applications and processes to retrieve them. We can have our script retrieve a password from a local keychain.

There are limitations to this approach:

  • the password item has to be created in the keychain
  • the user has to approve access to the password at least once
  • the keychain has to be unlocked when item is created and when the script runs—this usually requires the user to be logged in
  • the user and other scripts can find and read the password in the Keychain Access application or with the security tool

Because of these limitations, this approach is not useful for scripts that run without any user interaction, e.g. from a management system. Since the user can go and inspect the key in the Keychain Access is also not well suited for critical passwords and keys.

However, it is quite useful for workflow scripts that you run interactively on your Mac. This approach has the added benefit, that you do not have to remember to remove or anonymize any keys or passwords when you upload a script to GitHub or a similar service.

Note: Mischa used this in his ‘OnAirScanner’ script.

Update: I didn’t remember this, but Graham Pugh has written about this before.

How to Store a Password in the Keychain

Since adding the password to your keychain is a one-time task, you can create the password manually.

Open the Keychain Access application and choose “New Password Item…” from the Menu. Then enter the Keychain Item Name, Account Name and the password into the fields. The “Keychain Item Name” is what we are going to use later to retrieve the password, so watch that you are typing everything correctly.

You can also add the password from the command line with the security command.

> security add-generic-password -s 'CLI Test'  -a 'armin' -w 'password123' 

This will create an item in the Keychain with the name CLI Test and the account name armin and the horribly poor password password123.

How to Retrieve the Password in the Script

To retrieve a password from the keychain in a script, use the security command.

> security find-generic-password -w -s 'CLI Test' -a 'armin'

This will search for an item in the keychain with a name of CLI Test and an account name of armin. When it finds an item that matches the name and account it will print the password.

The first time you run this command, the system will prompt to allow access to this password. Enter your keychain password and click the ‘Always Allow’ button to approve the access.

This will grant the /usr/bin/security binary access to this password. You can see this in the Keychain Access application in the ‘Access Control’ tab for the item.

When you create the item with the security add-generic-password binary, you can add the -T /usr/bin/security option to immediately grant the security binary access.

Whether you grant access through the UI or with the command, keep in mind that a every other script that uses the security binary will also gain access to this password.

For very sensitive passwords, you can just click ‘Allow’ rather than ‘Always Allow.’ Then the script will prompt interactively for access every time. This is more secure, but also requires more user interaction.

Once you have tested that you can retrieve the password in the interactive shell, and you have granted access to the security binary, you can use command substitution in the script to get the password:

cli_password=$(security find-generic-password -w -s 'CLI Test' -a 'armin')

This command might fail for different reasons. The keychain could be locked, or the password cannot be found. (Because it was either changed, deleted or hasn’t been created yet.) You want to catch that error and exit the script when that happens:

pw_name="CLI Test"
pw_account="armin"

if ! cli_password=$(security find-generic-password -w -s "$pw_name" -a "$pw_account"); then
  echo "could not get password, error $?"
  exit 1
fi

echo "the password is $cli_password"