In the last post, we discussed how to run shell commands and scripts from an Apple Script environment. In this post, we will look at how we can run AppleScript commands and scripts from the shell environment.
Open Scripting Architecture
The key to running AppleScript from the shell is the osascript
command. OSA is short for ‘Open Scripting Architecture’ which is the framework that powers AppleScript. This framework allows AppleScript to have its native language, but also use JavaScript syntax.
The osascript
command allows us to run AppleScript commands from Terminal and shell. The most common use is the user interaction commands from AppleScript, like display dialog
:
osascript -e 'display dialog "Hello from shell"'
The -e
option tells osascript
that it will get one or more lines of statements as arguments. The following argument is AppleScript code. You can have multiple -e
options which will work like multiple lines of a single AppleScript:
> osascript -e 'display dialog "Hello from shell"' -e 'button returned of result'
OK
osascript
prints the value of the last command to stdout. In this case, it is the label of the button clicked in the dialog. (The ‘Cancel’ button actually causes the AppleScript to abort with an error, so no label will be returned for that.)
When you have multiple lines of script, using multiple -e
statements will quickly become cumbersome and unreadable. It is easier to use a heredoc instead:
osascript <<EndOfScript
display dialog "Hello from shell"
return button returned of result
EndOfScript
This also avoids the problem of nested quotation marks and simplifies shell variable substitution.
Shell variables and osascript
There are a few ways to pass data into osascript
from the shell.
Since the shell substitutes variables with their value before the command itself is actually executed, this works in a very straightforward manner:
computerName=$(scutil --get ComputerName)
newName=$(osascript -e "text returned of (display dialog \"Enter Computer Name\" default answer \"$computerName\")")
echo "New Name: $newName"
This works well, but because we want to use shell variable substitution for the $computerName
, we have to use double quotes for the statement. That means we have to escape the internal AppleScript double quotes and everything starts to look really messy. Using a heredoc, cleans the syntax up:
computerName=$(scutil --get ComputerName)
newName=$(osascript <<EndOfScript
display dialog "Enter Computer Name" default answer "$computerName"
return text returned of result
EndOfScript
)
echo "New name: $newName"
I have a detailed post: Advanced Quoting in Shell Scripts.
Environment Variables
Generally, variable substitution works well, but there are some special characters where it might choke. A user can put double quotes in the computer name. In that case, the above code will choke on the substituted string, since AppleScript believes the double quotes in the name end the string.
If you have to expect to deal with text like this, you can pass data into osascript
using environment variables, and using the AppleScript system attribute
to retrieve it:
computerName=$(scutil --get ComputerName)
newName=$(COMPUTERNAME="$computerName" osascript <<EndOfScript
set computerName to system attribute "COMPUTERNAME"
display dialog "Enter Computer Name" default answer computerName
return text returned of result
EndOfScript
)
echo "New name: $newName"
The shell syntax
VAR="value" command arg1 arg2...
sets the environment variable VAR
for the process command
and that command only. It is very useful.
Retrieving environment variables in AppleScript using system attribute
is generally a good tool to know.
Interpret this!
osascript
can also work as a shebang. That means you can write entire scripts in AppleScript and receive arguments from the shell. For example, this script prints the path to the front most Finder window:
#!/usr/bin/osascript
tell application "Finder"
if (count of windows) is 0 then
set dir to (desktop as alias)
else
set dir to ((target of Finder window 1) as alias)
end if
return POSIX path of dir
end tell
You can save this as a text file and set the executable bit. I usually use the .applescript
extension.
> print_finder_path.applescript
/Users/armin/Documents
To access arguments passed into a script this way, you need to wrap the main code into a run
handler:
#!/usr/bin/osascript
on run arguments
if (count of arguments) is 0 then
error 2
end if
return "Hello, " & (item 1 of arguments)
end
You can combine this into a longer script:
macOS Privacy and osascript
When you ran the above script, you may have gotten this dialog:
If you didn’t get this dialog, you must have gotten it at an earlier time and already approved the access.
AppleEvents between applications are controlled by the macOS Privacy architecture. Without this, any process could use AppleEvents to gather all kinds of data from any process. These dialogs are easy enough to deal with when running from Terminal. But if you put your AppleScript code (or shell scripts calling AppleScript) into other apps or solutions, it could get messy quite quickly.
Mac Admins generally want their automations to run without any user interactions. You can avoid these dialogs by creating PPPC (Privacy Preferences Policy Control) profiles that are distributed from an MDM server. In this case you have to pre-approve the application that launches the script, which can sometimes also be challenge. The other option is to find solutions that avoid sending AppleEvents altogether.
I have a longer post detailing this: Avoiding AppleScript Security and Privacy Requests
osascript and root
Management scripts often run as a privileged user or root. In this case, certain features of AppleScript may behave strangely, or not at all. I generally recommend to run osascript in the user context, as detailed in this post: Running a Command as another User
Conclusion
AppleScript’s bad reputation may be deserved, because its syntax is strange, and often very inconsistent. Nevertheless, it has features which are hard to match with other scripting languages. You can use the strategies from this and the previous posts to combine AppleScript with Shell Scripting and other languages to get the best of both worlds.
Most of the inconsistencies with AppleScript come from the various application implementations, which is endemic to application scripting. PowerShell is no better when you veer from the core language into application scripting. Unfortunately, everyone has their own ideas on how things should work, and they ignore the core language when they implement automation in their apps.
But the core AppleScript language itself is really not that inconsistent, indeed, it’s changed little in years. The syntax is different from most other languages, but that’s not a good/bad thing.
There are some really strange decisions in the main language, like three different means of addressing file (file, alias, and POSIX file), plus file in The Finder and System Events dictionaries may behave differently… but yes, most of the inconsistencies stem from different implementations in different software, even different software from Apple. Agreed.
Armin wrote:
display dialog "Enter Computer Name" default answer "$computerName"
Do NOT use shell string substitution to insert arbitrary data into executable AppleScript code. Please. It’s all fun and laughter till someone rm-s their entire systemor worse, someone else’s. xkcd 327 should be mandatory on all CS and sysadmin exams. It’s frightening how often I see posts on SO, etc. teaching others to do it this way.
BTW, if you want to access environment variables from within an AppleScript shell script, you can import macOS’s Foundation framework and call NSProcessInfo’s environment(). I wrote an AppleScript ‘File’ library that simplifies and other CLI tasks (reading stdin, writing stdout, parsing argv):
github.com/hhas/applescript-stdlib