Avoiding AppleScript Security and Privacy Requests

AppleScript on macOS is a useful tool for pro users and administrators alike. Even though it probably is not (and shouldn’t be) the first tool of choice for many tasks, there are some tasks that AppleScript makes very simple. Because of this it should be a part of your ‘MacAdmin Toolbelt.’

AppleScript’s strength lies in inter-application communication. With AppleEvents (or AppleScript commands) you can often retrieve valuable information from other applications that would be difficult or even impossible, to get any other way. With AppleScript, you may even be able to create and change data in the target applications.

If you are in any way security and privacy minded this should raise your hairs. Up to macOS 10.13 High Sierra, any non-sandboxed app could use AppleScript and AppleEvents to gather all kinds of personal and private data from various script-enabled apps and services. It could even use script-enabled apps like Mail to create and send email in your name.

Since macOS Mojave, the Security and Privacy controls restricts sending and receiving AppleEvents. A given process can only send events to a different process with user approval. Users can manage the inter-application approvals in the Privacy tab of the Security & Privacy preference pane.

MacAdmins have the option of pre-approving inter-application events with a PPPC (Privacy Preferences Policy Control) configuration profile that is pushed from a DEP-enrolled or user-approved MDM.

Privacy approval

You can trigger the security approval from Terminal when you send an event from the shell to another process with osascript:

> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'

When you run this command from Terminal, you will likely get this prompt:

You will not get this prompt when you have approved or rejected the Terminal app to send events to this particular target application before. You can check the permissions granted by the user in the Automation section of Privacy tab in the Security & Privacy pane of System Preferences.

For any given source/target application combination, the prompt will only be shown once. When the user approves the privilege (“OK” button), future events will just be allowed.

When the user rejects the connection (“Don’t Allow” button), this event and future events will be rejected without further prompts. The osascript will fail and the AppleScript will return an error –1743.

> osascript -e 'tell application "Finder" to get POSIX path of ((target of Finder window 1) as alias)'
79:84: execution error: Not authorized to send Apple events to Finder. (-1743)

If you want to get the approval dialogs again, you can reset the state of the source application (Terminal) with the tccutil command:

> tccutil reset AppleEvents com.apple.Terminal

This will remove the Terminal application and all target applications for it from the Automation (AppleEvents) area in the Privacy pane and show dialogs for every new request going forward. This can be very useful during testing.

Dealing with rejection

You should write your code in a ways that it fails gracefully when access is not granted. in this case osascript will return an error:

if ! osascript -e ' tell app "Finder" to return POSIX path of ((target of Finder window 1) as alias)'
then
 echo "osascript encountered an error"
 exit 1
fi

However, osascript will return errors for all kind of failures with no easy way to distinguish between them. As an example, the above will also fail when there are no Finder windows open.

If you want to distinguish AppleScript errors, you need to do so in the the AppleScript code:

if ! osascript -s o <<EndOfScript
    tell application "Finder"
        try
            set c to (count of Finder windows)
        on error message number -1743
            error "Privacy settings prevent access to Finder"
        end try
        
        if c is 0 then
            return POSIX path of (desktop as alias)
        else
            return POSIX path of ((target of Finder window 1) as alias)
        end if
    end tell
EndOfScript
then
    echo "osascript failed"
fi

Note: the -s o option of osascript makes it print AppleScript errors to standard out rather than standard error, which can be useful to find the errors in logs of management systems.

Note 2: when you are running osascript from management and installation scripts (which run as the root user) you need to run them as the current user to avoid problems.

Avoiding Privacy prompts

So, we know of one way to deal with the privacy prompts. Ideally, you would want to avoid them entirely. While this is not always possible, there are a few strategies that can work.

Don’t send to other Processes

In past versions of Mac OS X (I use this name intentionally, it’s that long ago.), scripts that showed dialogs might not display on the highest window layer. In other words, the dialog was lost behind the currently active windows. To avoid “lost” dialogs, it became best practice to send the display dialog command (and similar) to a process that had just received an activate command as well:

tell application "Finder"
    activate
    display dialog "Hello, World!"
end tell

As an alternative for Finder, the System Events process is often used as well. Jamf MacAdmins often used “Self Service.” This had the added bonus, that the dialog looks as if it comes from the Finder or Self Service, including the bouncing dock icon.

Over time, even though the underlying problem with hidden dialog has been fixed, this practice has persisted. You often even see AppleScript code use this with commands other than user interaction, where it wouldn’t have made sense in the first place. With the privacy restrictions in macOS Mojave, this practice has become actively trouble some, as you are sending the display dialog (or other) command to a separate process. The process running this script will require approval to send events to “System Events.”

osascript <<EndOfScript
    tell application "System Events"
        activate
        display dialog "Hello, World!"
    end tell
EndOfScript

In current versions of macOS, you can just use display dialog and may other commands without an enclosing tell block. Since your AppleScript code isn’t sending events to another process, no privacy approval is provided. This code has the same effect as above, but does not trigger an approval request.

osascript <<EndOfScript
    display dialog "Hello, World!"
EndOfScript

To determine whether an AppleScript command requires a tell block, you have to check where it is coming from. Many AppleScript commands that are useful to MacAdmins are contained in the ‘StandardAdditions’ scripting extension. Scripting extensions, as the name implies, extend the functionality of AppleScript without requiring their own process.

The useful commands in the Standard Additions extension include:

  • user interaction: choose file/folder/from list, display dialog/alert/notification
  • file commands: mount volume
  • clipboard commands: get the clipboard, set the clipboard to
  • sound control: set volume, get volume settings
  • system info

When your script uses only these commands, make sure they are not contained in tell blocks. This will avoid unnecessary prompts for access approval.

Exempt AppleScript commands

Some AppleScript commands are treated differently and will not trigger privacy approval:

  • activate: launch application and/or bring to front
  • open: open a file
  • open location: open a URL
  • quit: quit the application

For example, this will work without requiring approval:

osascript <<EndOfScript
    tell application "Firefox"
        open location "https://scriptingosx.com"
    end
EndOfScript

Use non-AppleScript alternatives

Sometimes, similar effects to an AppleScript can be achieved through other means. This can be difficult to figure out and implement.

As an example, I used this AppleScript command frequently for setup before Mojave:

tell application "Finder" to set desktop picture to POSIX file "/Library/Desktop Pictures/BoringBlueDesktop.png"

While Mojave was in the beta and it wasn’t really clear if or how the PPPC exemptions could be managed, I looked for a different means. I discovered Cocoa functions to read and change the desktop picture without triggering PPPC, and built a small command line tool out of that: desktoppr.

The downside of this approach is that you know have to install and/or manage a command line tool on the clients where you want to use it. There are different strategies for this, but it is extra effort compared to “just” running an AppleScript.

Build PPPC profiles to pre-approve AppleEvents

Even after you have considered the above options to avoid sending AppleEvents to another process, there will still be several situations where it is necessary. For situations where a MacAdmin needs to run a script on several dozens, hundreds, or even thousands of Macs, user-approval is simply not a feasible option.

MacAdmins can pre-approve AppleEvents (and most other privacy areas) between certain processes with a Privacy Preferences Policy Control (PPPC) configuration profile. PPPC profiles can only be managed when pushed from a user-approved or automatically enrolled MDM.

You can build such a profile manually, but it is much easier to use a tool to build these:

Your MDM solution might have a specific tool or web interface for this, consult the documentation or ask you vendor.

There is one big requirement here, though: only applications and tools that are signed with a valid Apple Developer ID can be pre-approved this way, as the signature is used to identify and verify the binary.

Determining the process that needs approval

While you can sign shell scripts and other scripts this is often not necessary. As we have seen earlier, when we ran our script from Terminal, it wasn’t the script that requested approval but the Terminal application. When your scripts run from a management system or another tool, it may not be easy to determine which process exactly needs approval.

The most practical approach to determine this, is to log the output of the ’Transparency, Consent, and Control” system (tcc) and look which process is sending the requests.

First, either use a clean test system, or reset the approvals for the processes that you suspect may be involved with tccutil.

Then open a separate Terminal window and run this command which will show a stream of log entries from the tcc process:

> log stream --debug --predicate 'subsystem == "com.apple.TCC" AND eventMessage BEGINSWITH "AttributionChain"'

There will be a lot of noise in this output.

Then run the script in question, the way you are planning to run it during deployment. If you are planning to run the script from a management system, then do that right now. You will get a lot output in the stream above.

Even when you don’t have a good idea what the parent process is going to be, you can filter the output for osascript since this is usually the intermediary tool used.

In my example I found several entries similar to this:

   0    tccd: [com.apple.TCC:access] AttributionChain: RESP:{ID: com.barebones.bbedit, PID[1179], auid: 501, euid: 501, responsible path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit', binary path: '/Applications/BBEdit.app/Contents/MacOS/BBEdit'}, ACC:{ID: com.apple.osascript, PID[18756], auid: 501, euid: 501, binary path: '/usr/bin/osascript'}, REQ:{ID: com.apple.appleeventsd, PID[577], auid: 55, euid: 55, binary path: '/System/Library/CoreServices/appleeventsd'}

The important information here is the responsible path which give me the binary and the enclosing application that tcc considers ‘responsible.’ This is the application you need to approve.

When you are running your scripts from a management system, your MDM vendor/provider should already have documentation for this, to save you all this hassle.

With all this information, you can build the PPPC profile with one of the above tools, upload it to your MDM and push it to the clients before the deployment scripts run.

Conclusion

While the added privacy around AppleEvents is welcome, it does add several hurdles to automated administration workflows.

There are some strategies you can use to avoid AppleScripts triggering the privacy controls. When these are not sufficient, you have to build a PPPC profile to pre-approve the parent process.

macOS Version Big Sur Update

Versioning software is a surprisingly difficult task. The version number conveys technical information: how much change can you expect and might it break existing workflows and data? Users will be more willing to pay for a major upgrade with new features, so the version number is used as a marketing tool. But for developers and MacAdmins, the version number has to be as granular as possible, ideally with a different number for each build made.

Because of these, sometimes opposing, interests, it is no wonder that versioning is often a problem for MacAdmins.

A brief history of the Mac operating system versions

Before the “Mac OS” label, the Macintosh operating system was called “System 7.” “Mac OS 8”, code-named “Copland,” was supposed to be the new operating system with all the new features, but the project kept slipping and then was cancelled. The “System 7.7” update was then re-named “Mac OS 8” to get Apple out of some third party licensing deals, which were set to expire on version 8. Marketing and legal matters decided the versioning here. (But it wasn’t all just marketing: Mac OS 8 also got the new interface design that had been created for Copland.)

When Apple announced the NextSTEP based major overhaul for the Macintosh in the late nineties, they chose to not give it a different name and new version numbers. Instead they chose to label the new system as “Mac OS X”, where the “X” was read as the roman numeral ten. I assume this was a marketing choice to demonstrate continuity with the previous versions, which had been “Mac OS 8” and “Mac OS 9.”

The first version of Mac OS X had the numerical version number 10.0.0 and got four “updates” to 10.0.4 Then it got the first major “upgrade” to 10.1. This broke with conventional versioning as major upgrades should get a new major number. When “Mac OS X 10.2 Jaguar” was announced at WWDC in 2002, it was obvious that Apple now considered “Mac OS X” a brand and would stick to the 10 in the system version numbers.

With the “Xserve,” the ‘X’ became a moniker to represent Apple’s “professional” or “EXpert” hardware and distinguish them from the ‘i’ prefix used for the consumer friendly devices and software. (iMac, iBook, iPod, iTunes, iMovie, iPhoto, etc.) Later “pro” software tools, such as “Xcode,” “Xsan,” and “Xgrid” picked up that prefix. Confusingly, the leading “X” was pronounced ‘ecks’ and the trailing ‘X’ in “Mac OS X” was still pronounced as ‘ten.’ Or at least that was what Apple marketing insisted it should be called.

In 2012, Apple dropped the “Mac” from the operating system for “OS X Mountain Lion” (10.8) The “X” represented the “pro” nature of Mac platform, as opposed to “iOS” for iPhone and iPad. Apparently, the “X” was considered a stronger brand for “pro” than “Mac” at the time…

This changed when Apple finally dropped the “X-as-ten” and returned the “Mac” with “macOS Sierra” (10.12) in 2016.

Even without the “X” in the system name, the ‘10’ has remained in the version number up to macOS 10.15 Catalina.

(The “X-as-ten” lives on with “Final Cut Pro X” and “Logic Pro X”. The prefix “X” lives on with “Xcode.”)

Big Sur goes to 11

The naming and versioning of the Mac platforms operating system was largely driven by symbolism, marketing and even legal matters. The same is true this year: Apple has finally given up on the “ten” and will raise the major version of macOS to 11.

I have a post on my opinion and reaction to macOS 11. Whatever the reasons, this is the year that we will get macOS 11.

When you go to “About this Mac” on a Mac running the Big Sur beta, it will proudly announce that it is running macOS 11.0. You get the same result when you run sw_vers:

% sw_vers -productVersion
11.0

This seems easy enough, however, there is a catch to this.

10.16 vs 11.0

Big Sur will not always report 11.0 as the macOS version. It might report 10.16 in some cases. These cases are meant for situations where software might just check the second part of the version for version checks.

The rules are detailed in this post from Howard Oakley.

In Big Sur you can force the compatibility mode by setting the SYSTEM_VERSION_COMPAT environment variable to 1:

export SYSTEM_VERSION_COMPAT=1
> sw_vers -productVersion
10.16

Checking the Version

When you have a script that checked the version of macOS (or Mac OS X or OS X), so far it was safe to ignore the first number of the product version and only compare the second. For example to see if macOS was Mojave or newer, you probably would have done something like this:

minorVersion=$(sw_vers -productVersion | awk -F. '{ print $2; }')
if [[ $minorVersion -ge 14 ]]; then
  echo "Mojave or higher"
fi

Now, with the release of macOS 11.0, this setup will return 0 for the minorVersion and fail.

The obvious solution here would be to extract the majorVersion as well and compare that first:

majorVersion=$(sw_vers -productVersion | awk -F. '{ print $1; }')
minorVersion=$(sw_vers -productVersion | awk -F. '{ print $2; }')
if [[ $majorVersion -ge 11 || $minorVersion -ge 14 ]]; then
  echo "Mojave or higher"
fi

This will work well enough, even when the script runs in a setup where it might get 10.16 as the version number. But is not particularly nice to read. Also, when you want to compare update versions, these will be (presumably) the minorVersion for Big Sur and later and the third part of the version number in Catalina and earlier and things will get even more messy quickly.

Maybe there is a better way of doing this than using the product (marketing) version of macOS?

Build Version

As I mentioned earlier, the user visible version may not be granular enough for the needs of developers. And because of this macOS has a second version, called the “build version”

The build version for the current version of macOS Catalina as I am writing this is 19G2021.

> sw_vers -buildVersion
19G2021

You can also see the build version in the “About this Mac” window when you click on the version number.

The build version consists of three parts and a fourth optional one. The first number is the Darwin Version. The following capital letter designates the update. The following number (up to four digits) is the specific build number. Sometimes the build number is followed by a lower case letter.

Darwin Version

This part of the version takes its name from the Darwin core of macOS.

The Darwin Version is number that is increased on every major release of macOS. Mac OS X 10.2 Jaguar was the first release of Mac OS X to consistently report its Darwin Version as 6. From that you can conclude that 10.0 had Darwin version 4 which makes sense, because it was the fourth release of NextSTEP, the operating system Mac OS X is based on.

macOS 10.15 Catalina, has a Darwin version of 19 and Big Sur reports 20.

You can also get the Darwin version in the shell from the OSTYPE environment variable:

> echo $OSTYPE
darwin19.0

But keep in mind that environment variable may not be set depending on the context your script runs in.

A safer way to get the Darwin version is with the uname command:

> uname -r
19.6.0

This Darwin version includes the update information.

Updates

In the build version updates are tracked with a capital letter. The letter A stands for the .0 or first release of a major version. B signifies the second release or first update or .1 release, and so on.

The current Catalina build version starts with 19G so we know it is the seventh release or sixth update to Catalina (10.15.6). The current Big Sur beta starts with 20A so it is the first release or .0 version.

Build numbers

The significance of the build number is most often seen during the beta phase. While the Darwin version and update letter are fixed during the beta phase, the build number increases with every beta release. This is the most granular number we have to determine the version.

For each update and major release the build number starts over, so it can only be used to compare releases for the same major release and update version.

Traditionally, there was a difference between two- and three-digit build numbers and four-digit build numbers. The builds with lower numbers of digits were general release builds, that will run on all Macs that support this version of macOS. The four digit build numbers designated either a security update or a hardware specific build.

Hardware specific builds occur when a new Mac model is released. These usually get a hardware specific build of macOS, since the drivers necessary for the new hardware are not included in the current general release version. Even though the product version numbers of macOS are the same for the general release and the hardware specific release, they have different build numbers.

Usually, the hardware specific drivers are merged into the general release on the next update. However, until the builds are merged, MacAdmins may have to manage hardware specific system installers and workflows for new hardware. This was especially annoying with the release of the 2018 MacBook Pro which had a specific build of 10.13.6 that was never merged into the general 10.13 build. MacAdmins that wanted or needed to support 10.13 had to manage a separate installer for these MacBooks.

Intruigingly, the Big Sur beta is different: its build number started in the 4000s and switched to the 5000s with beta 3.

Special builds

Some releases of macOS have a trailing lower case letter after the build number. This is particularly common during the beta phase. It is unclear what this letter designates exactly. It might designate that the installer application was re-built one or more times.

You can use regular expressions to parse out all the individual pieces of the build version:

if [[ $buildVersion =~ ([[:digit:]]{1,2})([[:upper:]])([[:digit:]]+)(.*) ]]; then
    darwinVersion=$match[1]
    updateLetter=$match[2]
    buildNumber=$match[3]
    specialBuild=$match[4]
else
    echo "couldn't parse build version"
    exit 2
fi

But in most cases, you will not need this level of detail.

Using the Build Version

The build version provides a way to compare macOS system versions that is not subject to the whims of marketing. We can even use it distinguish hardware specific builds of macOS from general versions or determine if security or supplemental updates have been applied.

For most comparisons, we only need the Darwin version and maybe the update.

The Darwin version has had two digits since Mac OS X 10.6 Snow Leopard. It is safe to assume that you won’t be managing Macs running 10.5 Leopard any more. (And if you do, they will probably be “hand-fed” and not subject to your deployment and update automations.) Assuming a two digit Darwin version, we can use string comparison to compare build versions:

# check if Mojave or higher
if [[ $(sw_vers -buildVersion) > "18" ]]; then
...

Since all versions of Mojave start with 18A... they are all alphabetically greater than 18. The same would go if you want to check for a maximum macOS version:

# check if Mojave or earlier
if [[ $(sw_vers -buildVersion) < "19" ]]; then
...

You can also filter for specific minimum updates:

# check if Mojave 10.14.6 or later
if [[ $(sw_vers -buildVersion) > "18E” ]]; then
...

By using the build version, we are avoiding all the trouble that the “marketing-driven” build version brings with it.

zsh solution

The above works for sh, bash and zsh scripts. However, when you are using zsh, there is another useful solution. zsh provides a function to compare versions called is-at-least.

When you use zsh in the Terminal interactively, it is probably already loaded, but when you want to use it in scripts, you should use autoload to make sure it is loaded. Then you can use is-at-least this way:

autoload is-at-least
# check for Mojave or higher
if is-at-least 10.14 $(sw_vers -productVersion); then
...

Since both 11.0 and 10.16 are higher than 10.14 this will work no matter what number Big Sur might be reporting, but if you want to check that the system is Big Sur, you want to use 10.16 as the minimum, which covers both possible values:

autoload is-at-least
# check for Big Sur or higher
if is-at-least 10.16 $(sw_vers -productVersion); then
...

Conclusion

The change of the version number in macOS 11 Big Sur might affect or even break some of your system version checking in your deployment and management scripts. There are some nice and easy solutions that are more resilient to changes in the “marketing” product version.

Installomator Updated: v0.3

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
  • improved logging in some statements
  • several more minor improvements

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

Some of the contributions and requests have not yet been addressed. I believe they will require some more thinking and planning. I would like to approach those in the next version.

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

Thanks again to all those who contributed!

Use scout to read Property Lists

I have written a few posts in the past about parsing data from property list files in scripts and Terminal. My usual tool for this is PlistBuddy. However, PlistBuddy’s syntax is… well… eccentric.

Recently, Alexis Bridoux, who is also the main developer on Octory, introduced a command line tool called scout which solves many of the issues I have with PlistBuddy.

For example, you can pipe the output of another command into scout, something you can only convince PlistBuddy to do with some major shell syntax hackery.

So instead of this:

> /usr/libexec/PlistBuddy -c "print :dsAttrTypeStandard\:RealName:0" /dev/stdin <<< $(dscl -plist . read /Users/armin RealName)


With scout I can use this much clearer syntax:

> dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]"


The tool can also modify existing files, by changing, adding or deleting keys.

scout can also parse JSON and (non plist) XML files, so it can also stand in as a replacement for jq and xpath. It will also color-code output for property list, XML and JSON files.

I have been using scout interactively in the Terminal for a while now. So far, I have been refraining from using scout in scripts I use for deployment. To use a non-system tool in deployment scripts, you need to ensure the tool is deployed early in the setup process. Then you also have to write your scripts in a way that they will gracefully fail or fallback to PlistBuddy in the edge case where scout is not installed:

scout="/usr/local/bin/scout"
if [ ! -x "$scout"]; then
    echo "could not find scout, exiting..."
    exit 1
fi

realName=$( dscl -plist . read /Users/armin RealName | scout "dsAttrTypeStandard:RealName[0]" )


All of this overhead, adds extra burden to using a tool. The good news is that scout comes as a signed and notarized package installer, which minimizes deployment effort.

I wills be considering scout for future projects. If anyone at Apple is reading this: please hire Alexis and integrate scout or something like it in macOS.

About bash, zsh, sh, and dash in macOS Catalina and beyond

This is an excerpt from my book: “Moving to zsh.” At the MacAdmins Conference Campfire session I received quite a few questions regarding this, so I thought it would be helpful information. You can get a lot more detailed information on “Moving to zsh” in the book!

Calls to the POSIX sh /bin/sh in macOS are handled by /bin/bash in sh compatibility mode. You can verify this by asking sh for its version:

% sh --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
Copyright (C) 2007 Free Software Foundation, Inc.

If Apple plans to eventually remove the bash binary, they need to have a replacement which handles sh.

Enter dash

Probably not coincidentally, Apple added the dash shell in Catalina. The Debian Almquist Shell (dash) is a minimal implementation of the Posix sh standard and commonly used on other UNIX and Unix-like systems as a stand-in for sh.

Apple also added dash (but not zsh) to the Recovery system in macOS Catalina. While sh is still interpreted by bash in both the Recovery system and the regular system, this is a strong indicator that Apple eventually wants to use dash as the interpreter for sh scripts.

When your scripts which use the #!/bin/sh shebang strictly follow the POSIX sh standard, you should experience no problems when Apple switches to ‘dash-as-sh.’

Tripping over bashisms

However, there are some quirks of the current ‘bash-as-sh’ implementation in macOS that you need to be aware of. When bash stands in as sh, it will nevertheless continue to interpret ’bashisms’—language features available in bash but not sh—without errors.

For example, consider the following script shtest.sh:

#!/bin/sh

if [[ $(true) == $(true) ]]; then
  echo "still good"
else
  echo "nothing is true"
fi

This script declares the #!/bin/sh shebang and it will work fine on macOS with bash-as-sh.

% shtest.sh
still good

However, when you try to run it with zsh-as-sh or dash-as-sh, it will fail.

You can make dash interpret the script instead of bash by switching the shebang to #!/bin/dash. But macOS Catalina has another, new mechanism available. In Catalina, the symbolic link at /var/select/sh determines which shell stands in as sh. By default the link points to /bin/bash:

% readlink /var/select/sh
/bin/bash 

When you change this link to either /bin/zsh or /bin/dash, the respective other shell binary will stand in as sh.

Switch the sh stand-in to dash with:

% sudo ln -sf /bin/dash /var/select/sh

And then run the script again:

% ./shtest.sh
./shtest.sh: 3 ./shtest.sh: [[: not found
nothing is true

When interpreted with dash instead of bash, the same script will fail. This is because dash is much stricter than bash in following the sh standard. Since dash is designed as a minimal implementation of the sh standard, it has to be stricter. The double brackets [[ … ]] are a ‘bashism,’ or a feature only available in bash and other, later shells such as ksh and zsh.

Even though zsh also interprets most of these bashisms, zsh in sh compatibility mode is also stricter than bash and will error.

You can switch back to the default bash-as-sh with:

% sudo ln -sf /bin/bash /var/select/sh

Since macOS has been using bash-as-sh for a long time, there may be many such bashisms lurking in your sh scripts. You can change the above symbolic link to test your scripts with dash-as-sh.

Some common ‘bashisms’ are:

  • double square brackets [[ ... ]]
  • here documents and strings (<<< and << operators)
  • double equals operator == for tests

Shellcheck to the rescue

You can also use the shellcheck tool to detect bashisms in your sh scripts:

% shellcheck shtest.sh                                          

In shtest.sh line 3:
if [[ $(true) == $(true) ]]; then
   ^----------------------^ SC2039: In POSIX sh, [[ ]] is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC2039 -- In POSIX sh, [[ ]] is undefined.

When you change the double square brackets for single square brackets, then you get this:

% shellcheck shtest.sh

In shtest.sh line 3:
if [ "$(true)" == "$(true)" ]; then
               ^-- SC2039: In POSIX sh, == in place of = is undefined.

For more information:
  https://www.shellcheck.net/wiki/SC2039 -- In POSIX sh, == in place of = is undefined.

Conclusion

In Catalina Apple started warning us about the eventual demise of bash from macOS. Converting your existing bash scripts and workflows to zsh, sh, or bash v5 is an important first step. But you also need to consider that the behavior of sh scripts will change when Apple replaces the sh interpreter.

Installomator updated: v0.2

It’s been nearly a month since I introduced Installomator.

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
    improved logging
  • several new applications: count increased from 62 in v0.1 to 87 in v0.2

Thanks to all who contributed!

Also, if you haven’t already, you want to read Mischa’s guest post on using Installomator with Jamf Pro.

Random Desktop Background Color with desktoppr

File this one under: probably useless, but fun.

I recently updated desktoppr with the feature to control the scaling of a custom desktop picture. Because a scaled desktop picture might not cover the entire screen, macOS also allows you to choose a custom color to fill the remaining area. desktoppr v0.3 can be used to control both of these settings.

So, I thought I could use this to just set a random single color as the desktop background, similar to how I did it for Terminal windows.

macOS does not allow you to set no desktop picture. So I created a PNG file that is only a transparent background. This image is basically an invisible desktop picture and all you see on screen is the fill color. Then, you can set any fill color with desktoppr color.

I have added a randombackgroundcolor script to the examples in the desktoppr repository on GitHub.

It’s a bit silly, but also kind of fun. Enjoy!

Update: desktoppr v0.3

I have posted an update for desktoppr. You can download it from the repository’s releases page.

This update adds two new verbs; scale and color.

Image Scaling

You can use scale to control how the desktop pictures are scaled. I have matched the options to the options in the Desktop preference pane:

  • fill: scale the image up or down so that it fills the entire screen (this is the default behavior)
  • stretch: stretch the image non-proportionally along both axes to fill the screen
  • center: show the image in the center in the original size
  • fit: scale the image up or down so that all of the image is shown
> desktoppr scale center

Background color

The center and fit options for image scaling may result in the desktop picture not fully covering the entire desktop. You can then control the background color with the color verb. The color verb takes a single hex coded color (as in web colors) (no leading # required):

desktoppr color 000000      # black background
desktoppr color FFFFFF      # white background
desktoppr color FF0000      # red background

Future of desktoppr

This tool has always been meant to be a simple ‘one-trick-pony.’ The option to control the image settings has been nearly since I published the first version. I am glad I have finally gotten around to implementing it.

I have learnt a lot about Swift since I first built this tool. When I look at the code now, I want to re-write the entire thing from scratch. I’d also like build better argument parsing. However, it does what it is supposed to do and if I rewrote it now it would probably change the syntax, breaking other people’s workflows

I don’t expect the tool will need updates, other than when it has to adapt to future macOS updates, but we will see.

Strategies to using desktoppr

A while back I introduced desktoppr. It is a very simple tool; its singular goal is to set the desktop picture (users and admins migrating from Windows call it ‘wallpaper,’ but that is not the proper macOS nomenclature) without requiring PPPC/TCC whitelisting.

The good news is that desktoppr still works fine, nearly one-and-a-half years in! Even though Catalina brought in many changes and restrictions, desktoppr kept doing its job.

Nevertheless, as I have used desktoppr myself in several different deployments, and I have a few different approaches to deployment, depending on the requirements.

Catalina split system volume

One of the new features of Catalina is a read-only system volume, separate from the data volume. This means that the pre-installed desktop pictures are no longer in /Library/Desktop Pictures/ but can now be found in /System/Library/Desktop Pictures. This is on the read-only part of the file system.

On a new “fresh” macOS installation, the /Library/Desktop Pictures does not exist. However, when you create this folder, its contents will appear in the ‘Desktop’ preference pane, merged with the contents of the protected system folder. So, we can continue to use /Library/Desktop Pictures as a place to store and install custom desktop image files.

Note: if you do not want the custom desktop picture to appear in the Desktop preference pane, then you can install the file in a different location. /Users/Shared or /Library/MyOrganization/ are useful locations.

Packaging the custom picture

> mkdir -p BoringDesktop/payload

> cd BoringDesktop
> cp /path/to/BoringBlueDesktop.png payload

> pkgbuild --root payload --install-location "/Library/Desktop Pictures/" --identifier com.example.BoringDesktop --version 1 BoringDesktop.pkg


These command will create a payload folder, copy an image file (my example is BoringBlueDesktop.png) and build an installation pkg using the pkgbuild command.

If you want a more detailed explanation of this process, you can find it in my book: “Packaging for Apple Administrators

Lock the Desktop

In classroom, lab, and kiosk settings, MacAdmins may want to set and lock the desktop picture. In this use case, you do not need desktoppr at all.

Use the above pkg to install the image file and then use your management system to push a configuration profile that sets and locks a Desktop Picture.

Many management systems will have the desktop picture controls hidden in the ‘Restrictions’ payload among many other settings. Please consult the documentation. You can also use this custom profile that only controls the desktop setting.

Preset the desktop, but the let user change it

This is the most common way MacAdmins will want to deploy is to pre-set a custom Desktop Picture but allow the user to change it later. This is what desktoppr was created for.

There are two approaches you can take to do this. Well, to be honest, there are way more, and all of them are valid, as long as they work. I should say: I will show two different approaches.

The modular approach

In this case you use your management system to install and run all the pieces:

  • install the custom desktop picture using the above pkg
  • install desktoppr using the pkg*
  • run a script that sets the desktop

* for 10.14.3 and earlier, desktoppr v0.2 requires the Swift 5 runtime support for command line tools to be installed.

The advantage of this approach is that we already did the first part earlier, and the desktoppr pkg can be downloaded from the git repo,. So, we already have two of the three parts.

For the script, there is a sample script in the repository, as well.

Note that this script has changed slightly since the last post. Originally, the script used launchctl asuser. The behavior of launchctl asuser seems to have changed somewhat in a recent update and I have switched the script to use sudo -u instead.

This approach can be used with Munki, Jamf, outset, and many other management solutions.

All in one package

The downside of the modular approach is that you have to manage three pieces (the image file, the desktoppr binary, and a script) in your management system. This can be especially problematic when you are not the actual administrator of the management system but more active in a ‘consulting role.’

In this case, I have found it easier to build a single package that does all the work. This is easier to hand over to another admin. It is also more flexible and can be used in even more situations. It is a bit more work to assemble, though.

First, you need the ‘ingredients:’

* for 10.14.3 and earlier, desktoppr v0.2 requires the Swift 5 runtime support for command line tools to be installed.

First, create a project folder with a payload folder inside. Then copy the necessary files into the right place:

> mkdir -p BoringDesktop/payload
> cd BoringDesktop


Copy the image file to payload:

> cp /path/to/BoringBlueDesktop.png payload


Create a scripts directory and copy the postinstall script to it:

> mkdir scripts
> cp path/to/desktoppr/examples/postinstall scripts


Expand the zip archive (in Finder) and copy the desktoppr binary into the scripts folder.

> cp path/to/build/usr/local/bin/desktoppr scripts


Your project folder should now look like this:

BoringDesktop Project FolderBoringDesktop Project Folder

You can then build the pkg with:

> pkgbuild --root payload --scripts scripts --install-location "/Library/Desktop Pictures/" --identifier com.example.BoringDesktop --version 2 BoringDesktop-2.pkg


Note the different version number, so the system can recognize this as a different pkg from the one you might have built earlier.

This form of the pkg does not actually install the desktoppr binary on the target system. When the pkg is created, the entire contents of the scripts folder will be archived into the pkg file. When the pkg is being installed, the contents will be expanded into a temporary directory. That means that the postinstall script can see the binary in the same director ‘next to it.’ This happens in lines 22–27 of the postinstall script.

After the installation is complete, the temporary files will be removed, so the postinstall script and the desktoppr binary will be removed automatically. You don’t need to worry about the cleanup.

Conclusion

Which approach works best depends on your specific deployment needs, your management setup and workflows and (not the least) your comfort with building scripts and packages.

Even when you have defined your deployment needs, there are multiple solutions on how to build and deploy a custom desktop picture. As long as they achieve the desired goal, there is no “best” solution.

You can earn more details about building packages in my book: “Packaging for Apple Administrators

macOS shell command to create a new Terminal Window

Of course, you can easily create a new Terminal window from the ‘Shell’ menu or by using the ⌘N (or ⌘T) keyboard shortcut. But in some cases, it can be more useful to use a shell command.

New windows created with the keyboard shortcut or from the menu will always have the home directory ~ as the current working directory. What I want, is a new window that defaults to current working directory or a custom directory that I can provide with an argument:

> new           # opens a new terminal window at the current working directory
> new ~/Desktop # opens a new terminal window at ~/Desktop

No luck with AppleScript

After my last success using AppleScript, I thought this would be the best solution again. Unfortunately, this particular piece of the AppleScript dictionary is broken. The make new window or make new tab commands fail with errors and I have tried several combinations.

After some web searching, it looks like this has been broken for a long time. I filed an issue in Feedback Assistant.

You can create a new Terminal window with AppleScript using the do script command in the Terminal dictionary. (Not to be confused with do shell script.) So this AppleScript, sort of does what I want, but seems cumbersome.

tell application "Terminal"
    do script "cd ~/Desktop"
end tell

If you know of a better way to create a new Terminal window or, even better, a Terminal tab with AppleScript, then please let me know. (No UI Scripting solutions – those have their own issues.) I have a few other ideas where this might come in useful.

Enter the open command

During those web searches, I also found suggestions to use the open command, instead:

> open -a Terminal ~/Documents

Will open a new Terminal window with ~/Documents as the working directory. This is already really close to what I wanted.

I created this function in my shell configuration file (bash, zsh):

# creates a new terminal window
function new() {
    if [[ $# -eq 0 ]]; then
        open -a "Terminal" "$PWD"
    else
        open -a "Terminal" "$@"
    fi
}

With this, I can now type

> new Projects/desktoppr

and get a new Terminal window there. This is very useful when combined with the history substitution variable !$ (last argument of previous command):

> mkdir Projects/great_new_tool
> new !$

And an unexpected, but useful side effect is that the new function can also open an ssh session in a new window:

> new ssh://username@computer.example.com

Hope you find this useful, too!