Open Apps with custom Shortcuts in macOS

Someone on the MacAdmins Slack recently asked how you could assign a global keyboard short cut to open Terminal on macOS.

Note: alternative terminal applications such as iTerm2 may have this built-in.

macOS has an option to assign custom global keystrokes to pretty much anything, but it is not obvious how to get there.

  • First, open the Automator application. In the chooser for a new Workflow, choose ‘Quick Action’ (on Mojave) or ‘Service’ on earlier versions of macOS.
The new Workflow chooser in Mojave
The new Workflow chooser in Mojave
  • In the new workflow configure the input to be ‘no input’ and the application to be ‘any application.’
  • Then search for ‘Launch Application’ action in the library pane on the left and add it to your workflow by double-clicking or dragging.
  • The popup menu where you can slect an application in the action will only show applications from the /Applications folder. Choose ‘Other…’ and select Terminal in the ’/Applications/Utilities` folder.
Configure your workflow
Configure your workflow
  • Save the workflow. Give it a meaningful name such as ‘Open Terminal.’ Since you chose Quick Action or Service, this workflow will be saved in ~/Library/Services.
  • Open System Preferences > Keyboard. Click the ‘Shortcuts’ tab and select ‘Services’ from the list on the left side. (Even on Mojave, it is still called ‘Services’.)
  • Scroll all the way down the list of services under the ‘General’ heading, you should find the service you just created. Select it and click ‘Add Shortcut’ to assign a global shortcut.
Keyboard Shortcut Preferences
Keyboard Shortcut Preferences
  • You are done!

When the active application uses the same keystroke, the application’s definition will precede your global shortcut.

Of course, you don’t have stop at launching applications. You can assign a global keyboard shortcut to any Automator workflow this way. Since Automator workflows can include AppleScript, Python or shell scripts, you can do pretty much anything this way!

However, most Apple users don’t bother with shortcuts to launch apps. Just invoke Spotlight with command-space and start typing term and hit return.

Learn Scripting at Pro Warehouse!

I am really excited about this!

Pro Warehouse is extending their services with training for Mac IT specialists. We call it the ‘Pro Academy.’ As part of that, we are going to offer two-day Scripting macOS classes!

The first class is “Introduction to Scripting macOS.”

The point of this class is to overcome the first hurdles of a very daunting and complex topic. You will learn the basics of bash scripting (and debugging). The goal you can start scripting and gain experience on your own, without getting into too much trouble. The class is designed specifically for Mac adminstrators, so most of the examples and exercises will have direct application for Mac system management. You will learn both scripting and some useful adminstration tools.

Later, we will also offer an “Advanced Scripting macOS” class, that builds on the first, which will go deeper and address more complex topics.

The Scripting classes are designed and held by myself. I do have to thank the entire team at Pro Warehouse for the amazing effort everybody put in to make this happen.

The Scripting classes (and our other management classes) will be offered in our new training facility at the Pro Warehouse offices in Amsterdam! Classes will be offered in English, so international participants are welcome!

Register here!

I am looking forward to seeing you there!

Build Simple Packages with Scripts

In a past post, I described how path_helper works. As an example, I mentioned the installer for Python 3 which runs a postinstall script that locates and modifies the current user’s shell profile file to add the Python 3 binary directory to the PATH.

Not only is modifying a user’s file a horrible practice, but it will not achieve the desired purpose when the user installing the package is ultimately not the user using the system. This setup happens fairly frequently in managed deployment workflows.

As described in the earlier post, macOS will add the contents of files in /etc/paths.d/ to all users’ PATHs. So, all we have to do is create a file with the path to the Python 3 binary directory in /etc/paths.d/. A perfect task for a simple installer package.

The steps to create such an installer are simple:

$ mkdir -p Python3PathPkg/payload
$ cd Python3PathPkg
$ echo "/Library/Frameworks/Python.framework/Versions/3.7/bin" > payload/Python-3.7
$ pkgbuild --root payload --install-location /private/etc/paths.d --version 3.7  --identifier com.example.Python3.path Python3Path-3.7.pkg
pkgbuild: Inferring bundle components from contents of payload
pkgbuild: Wrote package to Python3Path-3.7.pkg

This is not so hard. However, since the path to binary contains the major and minor version number, you will have to create a new version when Python 3 updates to 3.8 (and 3.9, etc…).

So, it makes sense to script the process. With a package this simple, you can create everything required to build the package (i.e. the payload folder with contents) from the script in a temporary directory and then discard it after building.

You can find my script at this Github repository.

Note: when you modify the PATH with path_helper, your additions will be appended. The Python 3 installer prepends to the PATH. This might lead to slightly different behavior, as the Python 3 behavior overrides any system binaries. If you want to prepend for every user, you have to modify the /etc/paths file.

There are a few other simple installers where this approach makes sense. I also made a script that builds a package to create the .AppleSetupDone file in /var/db to suppress showing the setup assistant at first boot. Since I was planning to use this with the startosinstall --installpackage option, this script builds a product archive, rather than a flat component package.

You could create this package once and hold on to it whenever you need it again, but I seem to keep losing the pkg files. The script allows you to easily re-build the package in a different format or sign it when necessary. Also, dealing with the invisible file is a bit easier when you just create them on demand.

The last example creates a single invisible file .RunLanguageChooserToo, also in /var/db/. This will show an additional dialog before the Setup Assistant to choose the system language. MacAdmins might want to have this dialog for the obvious reason, but it also allows for a useful hack. When you invoke the Terminal at the Language Chooser with ctrl-option-command-T it will have root privileges, which allows some interesting workflows.

With this package the creation of the flag file happens too late to actually show the chooser. So I added the necessary PackageInfo flags to make the installer require a restart. Note that startosinstall will only respect this flag with a Mojave installer, not with High Sierra.

These three scripts can be used as templates for many similar use cases. As your needs get more complex, you should move to pkgbuild scripts with a payload folder, munkipkg, or Whitebox Packages.

You can learn about the details of inspecting and building packages in my book: “Packaging for Apple Administrators”

User Interaction from bash Scripts

As MacAdmins our goal is to automate workflows when ever possible. The advantages of automation are great. You immediately reduce the workload, but also reduce the potential for someone to make a mistake, which would mean even more work later.

In nearly all cases, we want the automation to happen “magically” in the background, without any user interaction. User interaction slows down the process as the script is waiting for the user to confirm or enter data. User input also requires validating and checking user entered data.

In most cases, when you believe you need to prompt the user for, well, anything, you should take that moment to re-think your workflow. Maybe you can come up with a different workflow that can provide the data from a source that can be automated without user interaction.

If, however, you really are convinced that user interaction is necessary, then you need be aware there are many potential pitfalls in writing shell scripts with user interaction.

macOS Mojave will introduce even more pitfalls with its increased security features.

Do you really need UI?

A common example for user interaction is to get and set the Computer Name or Asset Tag/ID/Number from manual input.

This is a task that can be fully automated in many situations. As an admin you could provide a text file mapping serial numbers to a name and/or asset tag on a server, that a script can download (curl) and parse. If you have a management system or asset database, there is probably an XML or JSON API, you can use to retrieve this information from a script. You could read or scan the serial numbers from the labels of the Mac’s boxes or even get the list with the purchase order from most vendors. With that data you can pre-fill your text files or databases.

However, in some cases, especially DEP workflows it may be difficult or impossible to predict which computer will end up on which desk, especially with zero-touch deployment workflows, where a device can be sent to a user directly.

In this case, you have to question whether you need a unique, specific computer name or asset tag. Maybe the data which your management system already gathers, such as the user who enrolled the device and its serial number will (have to) be sufficient?

In many cases, however, the reasons to prompt for and set a computer name will not be technical but stem from other, external factors that the IT department may or may not have influence on.

AppleScript’s Moment of Glory

bash was built to run in a text based terminal on many different operating systems. It has (and should have) no concept of a graphical user interface. bash alone is not useful to interact with the user, unless you want to open a terminal window.

However, AppleScript does have some (basic) commands to present dialogs and notifications to the user. We can call AppleScript command from the shell with the osascript command (OSA = open scripting architecture, the underlying framework that AppleScript uses)

There are other tools that allow to display user interface from a shell script. However, they all require an additional installation. AppleScript/osascript is simpler and built-in to the OS, so it is my first choice. That doesn’t mean it is always the appropriate choice, though.

You can go to Terminal and make a dialog appear with

$ osascript -e 'display dialog "Hello from bash!"'

The display dialog AppleScript command is documented in the “StandardAdditions” dictionary. You can see it when you choose ‘Open Dictionary…’ from the File Menu in Script Editor. Then select the “Scripting Additions.osax” dictionary and choose the “User Interaction” category.

This command has a lot of options that allow us to configure the dialog. You can experiment and test with these commands in the Script Editor application. For example, you can change the names of the buttons:

display dialog "Are you sure!?" buttons {"No", "Yes"}

You can also have just one button:

display dialog "Just accept it!" buttons {"Accept"} default button 1

Or three: (three is the maximum)

display dialog "The answer is C" buttons {"A", "B", "C"} default button 3
button returned:C

The script will return which button the user clicked. However, instead of parsing the output with shell tools, you should let AppleScript do the work:

button returned of (display dialog "Are you sure!?" buttons {"No", "Yes"})

The choice variable will be No or Yes.

Adding icons

You can also add an icon to the dialog:

display dialog "Hello" with icon note

Will show the current application’s icon. You can also use stop or caution for different icons.

You can also add a path to an icns file:

display dialog "Hello!" with icon POSIX file "/Applications/"

Asking for Input

In some cases you want to get information back from the user. You can add a text field to the dialog with the default answer argument. The default answer can be an empty string:

display dialog "Who are you?" default answer "nobody"
display dialog "Who are you?" default answer ""

You can get the result of the dialog with the text returned property:

text returned of (display dialog "Who are you?" default answer "nobody")

You can combine the default answer argument with all the above arguments as well.


In some case you just want to notify the user that something happened, and not stall everything while the script waits for confirmation. You can use display notification for these situations:

display notification "Hello, again" with title "Hello"

Running from Shell

You can execute AppleScript from the shell with the osascript command.

osascript -e 'display dialog "Hello!" with icon note'

This works well enough. You also use shell variable substitution:

title='Hey, there!'
osascript -e "display dialog \"What's up?\" with title \"$title\""

When you use variable substitution, you have to use double quotes for the command strings. And then you have to escape any additional double quotes in the AppleScript command. With more complex commands and arguments this will get unwieldy very quickly.

In a bash script you can use a here document instead:

title='Hey, there!'
osascript <<-EndOfScript
display dialog "What's Up?" with title "$title"

You start a here document with the <<- characters followed by a limit string of your choice (I chose EndOfScript). Then all the text until the limit string is repeated will be fed into the osascript command.

Bash variables will be substituted in the here document, so $title will be substituted with its contents.

Note: I prefer the <<- syntax over the << syntax with osascript. The <<- syntax will ignore leading white space in the following lines, allowing me to properly indent the AppleScript code, making the entire script more legible.

The Trouble with root

Management scripts will run with root privileges. They may also be run in situations where no user is logged in. AppleScript requires a user to be logged in (and a window server to be present) to display the alert.

In many situations all of this will ‘just work’ even when the script is executed from a root process, in others, the script will fail. It is generally safer to always check the current user and run the osascript command as the currently logged in user. You can learn about how and why to do this in this article on ‘Root and Scripting’.

This also gives your script an option to continue silently or fail when no user is logged in.

user=$(python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");')
if [[ $user != "" ]]; then
    uid=$(id -u "$user")
    launchctl asuser $uid /usr/bin/osascript -e "button returned of (display dialog \"Hello\")"

Putting it all Together

While all of these examples are simple enough, you can already see that, once you consider all the possible combinations, everything will get fairly complex.

Over time I have put together a few bash functions that I use in my scripts. Even they only cover quite simple workflows, but can be useful as a sample for more complex needs:

System Events, Privacy and macOS Mojave

In many scripts the author wraps the display dialog command wrapped in a tell statement:

osascript -e 'tell app "System Events" to display dialog "Hello"'

or, like this:

osascript <<-EndOfScript
tell application "Finder"
    display dialog "Hello"
end tell

The reason for this is that it will ensure that the dialog is displayed on top of all other windows. The activate command will push the targeted process to the front. Both “System Events” and “Finder” are used frequently.

In most cases this is not necessary, as the dialog will properly display on top of all other windows, even as a standalone command. There may be some weird conditions when other applications are launched in the same time frame, though. In other cases it may be better to use display notification, anyway.

In macOS Mojave, Apple Events (AppleScript Commands) can only be sent to another application with user permission.

When you try this command in macOS Mojave, the user will be prompted to allow the Terminal application to control the System Events application

Apple Event permission dialog

If the user denies this request, the osascript command will fail with an error:

$ osascript -e 'tell app "System Events" to display dialog "Hello"'
28:50: execution error: Not authorized to send Apple events to System Events. (-1743)

Once a user has denied access, they will not be prompted again. They will have to go to the ‘Privacy’ tab in the ‘Security & Privacy’ pane in System Preferences, search for ‘Automation’ and allow access for Terminal.

Security & Privacy Pane in macOS Mojave

However, management scripts will usually not be executed from Terminal, but from within the context of an installation script or management agent. It may be possible to pre-approve configurations with a UAMDM configuration profile, but it will be impossible to anticipate all contexts in which your scripts may run.

For macOS Mojave it will be better to either modify your script to not wrap the command in a tell statement. Alternatively, you can use a different solution for the UI altogether. (e.g. jamfHelper, Yo, Pashua or CocoaDialog)

All of these wrapped commands in scripts will break in macOS Mojave!

MacAdmins need to go through all their management scripts and check for AppleScript UI commands wrapped in tell statements.

When you use the standalone display dialog it will be the AppleScript process itself that displays the dialog. This requires no specific permission.

Even if your dialogs may not appear on top of all the windows, this is a preferable solution.

This will also affect scripts using osascript to control or receive data from other applications.


  • question and revisit every use of user interaction in your workflow
  • when you really have to, you can use osascript with the display AppleScript commands
  • to be safe, run the commands as the current user
  • increased macOS Mojave security will require you to verify all your scripts using osascript