In this series of posts, I am exploring the various different ways of launching scripts on macOS. In the first two posts, we explored what happens when you launch scripts from Terminal. We already explored some concepts such as the shell environment and how that affects scripts. In this post we are going to explore a different, but very common way to launch shell commands and scripts: AppleScript’s do shell script
command.
do shell script
When AppleScript made the transition from Classic Mac OS 9 and earlier to Mac OS X, it gained one command that allowed AppleScripts to interact with Mac OS X’s Unix environment. The do shell script
command executes a command or script in the shell and returns the output as a text to the AppleScript.
We have used this in an earlier post:
do shell script "echo $PATH"
--> "/usr/bin:/bin:/usr/sbin:/sbin"
But you can use this to run any shell command:
do shell script "mdfind 'kMDItemCFBundleIdentifier == org.mozilla.firefox'"
--> "/Applications/Firefox.app"
Note the use of single quotes inside the double quotes which delineate the AppleScript text. I have a post on the challenges of quoting in these mixed scripting environments.
You can assemble the command you pass into do shell script
using AppleScript text operators:
set bundleID to "org.mozilla.firefox"
do shell script "mdfind 'kMDItemCFBundleIdentifier == " & bundleID & "'"
--> "/Applications/Firefox.app"
Note that the PATH
variable for AppleScripts that are run from Script Editor or as an AppleScript applet is different than the PATH
in your interactive environment. Most notably, it does not include /usr/local/bin
. When you want to use a command or script that is not stored in the four default directories, you will have to use the full path in the do shell script
:
do shell script "/usr/local/bin/desktoppr"
(Desktoppr is a small tool I built to work with desktop pictures on macOS, you can get it here.)
When you are unsure what the full path to a command is, you can use the which
command in Terminal:
> which desktoppr
/usr/local/bin/desktoppr
Errors
Keep in mind that which
uses the same logic to lookup a command as the shell does when it looks up a command with no path. So, if you think you can trick AppleScript by using the which
command to lookup a non-standard command, it will still fail:
do shell script "which desktoppr"
--> error "The command exited with a non-zero status." number 1
When the command in do shell script
returns a non-zero exit code, you will get an interactive dialog informing the user of the error. The AppleScript will not continue after the error. You can handle the error the same way you would handle any AppleScript error, with a try… on error
block:
set filepath to "/unknown/file"
try
do shell script "/usr/local/bin/desktoppr" & quoted form of filepath
on error
display alert "Cannot set desktop picture to '" & filepath & "'"
end try
Files and Paths
AppleScript has its own methods of addressing files and folders. Actually, there are multiple ways, which is one of the confusing things about AppleScript. Neither of the native forms of addressing files and folder in AppleScript use the standard Unix notation with forward slashes separating folders. But there are built-in tools to convert from Unix notation to AppleScript and back.
Use the POSIX path
attribute to get a Unix style file path from an AppleScript file
or alias
. Unix style paths used with commands need spaces and other special characters escaped. You can use the quoted form
attribute to escape any AppleScript string. This is usually used directly with POSIX path
:
set imagefile to choose file "Select a Desktop"
--> alias "Macintosh HD:Library:Desktop Pictures:BoringBlueDesktop.png"
set imagepath to quoted form of POSIX path of imagefile
--> '/Library/Desktop Pictures/BoringBlueDesktop.png'
do shell script "/usr/local/bin/desktoppr " & imagepath
You can convert a Unix style file path into an AppleScript file with the POSIX file
type:
set bundleID to "org.mozilla.firefox"
set appPaths to do shell script "mdfind 'kMDItemCFBundleIdentifier == " & bundleID & "'"
--> "/Applications/Firefox.app"
if (count of paragraphs of appPaths) = 0 then
display alert "No app found"
else
set appPath to first paragraph of appPaths
-- convert the path to an AppleScript file
set appFile to POSIX file appPath
--> file "Macintosh HD:Applications:Firefox.app:"
tell application "Finder" to reveal appFile
end if
Shell Scripts in AppleScript Bundles
Sometimes you are writing an AppleScript and want to use a script for some functionality which is difficult to achieve in AppleScript. When you have an AppleScript file and a shell script file that work together this way, you want to store them together and also have an easy way for one to get the location of the other.
For this, AppleScript provides the notion of script bundles and AppleScript applets (which are also bundles). A script bundle is not a flat file, but a folder which contains the script itself and other resources, such as script libraries, or shell scripts. The script can easily locate items in its bundle and call them.
For example, we want a script that needs to unzip a file. We can use the unzip
command line tool to do that, but in my experience it is better to use ditto
. The ditto
expansion seems to be closer to how the expansion from Archive Utility works and do better with extended attributes and resource forks and other macOS specific things.
This is the shell script for our example:
#!/bin/sh
# target dir for expansion
targetdir="/Users/Shared/Script Bundle Demo"
# sanity checks for argument 1/filepath
if [ -z "$1" ]; then
exit 2
fi
filepath=$1
# is it a file?
if [ ! -f "$filepath" ]; then
exit 3
fi
# note: ditto seems to work better than unzip
ditto -x -k "$filepath" "$targetdir"
This is simple enough that you could just do it in a one-line do shell script
, but I want to keep it simple. You could extend this shell script to use different tools to expand different types of archives, such as xar
, tar
, aa
etc. If you want a more complex script, feel free to build one!
Now we can build the AppleScript/shell script combination. Open Script Editor, create a new script and save it right away. In the save dialog, change the ‘File Format’ to ‘Script bundle’ before saving.
After you have saved the Script as a bundle, you can see the Bundle Info in a pane on the right side of the script window. If you don’t see this pane, choose ‘Show Bundle Contents’ from the ‘View’ menu, or click the right most icon in the tool bar.
In this pane, you can set the name, identifier, version and some other data for the script bundle. You can also see a list of the ‘Resources’ which shows the contents of the Contents/Resources
folder in the script’s bundle. When you find the script you save, you will see it has a scptd
file extension and when you open the context menu on it in Finder, you can choose ‘Show Package Contents’ and dig into the bundle contents.
Note: AppleScript applications (or applets) work the same way. Their .app
bundles have a few more sub folders, but the Resources work the same way. The difference is that AppleScript applets work on double-click, drag’n drop, and some other events that we will get to in later posts. Script bundles have to run from Script Editor.
Save the shell script from above into the script bundle’s Resources
sub-directory with the name unarchive.sh
. You should see it appear in the ‘Resources’ list in the script window.
This way, the AppleScript bundle can contain all the resources it might need, including shell (or other) scripts.
Now we still need to find a way to access the Resources from the script. To run our shell script, add the following code to the AppleScript in Script Editor:
-- Script Bundle Demo
set theArchive to choose file "Select a zip archive:" of type {"zip"}
set archivePath to quoted form of POSIX path of theArchive
-- assemble command
set scriptPath to quoted form of POSIX path of my (path to resource "unarchive.sh")
set commandString to scriptPath & space & archivePath
-- for debugging
log (commandString)
do shell script commandString
First we prompt the user to choose a file with a zip
extension, and the we convert the result into a quoted Unix path.
Then, we use the path to resource "unarchive.sh"
to get the path to our shell script in the Resources
folder in the bundle. Then we get the quoted Unix notation, and assemble the command for the do shell script
. The log
command there will print the commandString
to the output in the script window and is useful for debugging. Then we run the command with do shell script
.
Environment for do shell script
Our example script expands the archive into a subfolder of /Users/Shared
. If you wanted to use a different location, you could use a second argument in the script.
There is a different way of passing data into scripts and that is environment variables.
First of all it is important to note that the shell environment for commands and scripts run with the do shell script
command from an AppleScript in Script Editor or an AppleScript application is very different from the shell environment in an interactive shell in the Terminal. We have already seen that the PATH
environment variable has a different value, which influences the command lookup behavior.
You can check the environment variable by running the env
command. This will list all environment variables. (To be nitpicky, there is more to a shell environment than just the env variables, there are also shell options, but those will be different for each shell, sh, bash or zsh, anyway.)
do shell script "env"
--> "SHELL=/bin/zsh
TMPDIR=/var/folders/2n/q0rgfx315273pb4ycsystwg80000gn/T/
USER=armin
COMMAND_MODE=unix2003
__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0
PATH=/usr/bin:/bin:/usr/sbin:/sbin
__CFBundleIdentifier=com.apple.ScriptEditor2
PWD=/
XPC_FLAGS=0x0
SHLVL=1
HOME=/Users/armin
LOGNAME=armin
_=/usr/bin/env"
Interestingly, we have USER
and HOME
to use in this environment.
We can also add environment variables to a do shell script
command:
do shell script "TARGET_DIR='/Users/Shared/Script Bundle Demo' " & scriptPath & space & filePath
You can use this to set the value of the TARGET_DIR
env variable for the next command, which is our script in the script bundle.
Administrator Privileges
No matter which way you use do shell script
, it has one big benefit. You can prompt the user to get the command or script to run with administrative privileges:
do shell script "systemsetup -getRemoteLogin" with administrator privileges
This will prompt for the user name and password of an administrator user. This can allow to build some simple workflows that require user interaction and administrative privileges really easily.
Conclusion
Combining Script Bundles and AppleScript Applications with shell scripts can create a powerful combination. You can use the “best of both worlds” with some of AppleScript’s user interaction commands and the shell’s strength in file manipulation and similar workflows. You can also sign AppleScript applications with a valid Apple Developer ID and pre-approve them for privacy exemptions with a PPPC profile.
If this explanation is not detailed enough for you, there is an amazing Tech Note in Apple’s Documentation Archive.
This post covered launching shell scripts from AppleScript. In the next post we will launch AppleScript code from shell scripts.
Awesome write up! This would have saved past me several days of swimming through Google. Haha
I found the article very helpful.
I want to run a couple of shell scripts for simple tasks without opening Terminal windows.
So far, I did this by creating little Automator apps which execute the shell scripts. I don’t find this approach very elegant and Apple is about to discard Automator, so I’m looking for another solution.
Making the scripts executable and simply wrapping them in an app folder is a solution which I really like, but it doesn’t work on my new system. (macOS keeps asking me to install Rosetta.)
Creating little apps with Script Editor or osascript seems to be a quite straightforward solution. I like that the apps are much smaller than Automator apps.
I keep wondering about two things, though:
(1) Isn’t there a more simple solution for running shell scripts in the background than using AppleScript (or an an app which was compiled from AppleScript)?
(2) Is it more straightforward to use Automator for turning shell scripts into little apps than doing the same thing via AppleScript? I googled this quite thoroughly and got the impression that using Automator is considered a solid solution while making an app with Script Editor or osacript is rather considered an awkward workaround.
I would be very glad to get some feedback on this.
If Automator worked for you, why don’t you use the official replacement for Automator which is Shortcuts? Shortcuts for Mac has a ‘Run Shell Script’ action.