Script attachment enables advanced users to create powerful workflows that start in your app. NSUserScriptTask lets you implement script attachment even if your app is sandboxed. This post explains how to set that up.
IMPORTANT Most sandboxed apps are sandboxed because they ship on the Mac App Store [1]. While I don’t work for App Review, and thus can’t make definitive statements on their behalf, I want to be clear that NSUserScriptTask is intended to be used to implement script attachment, not as a general-purpose sandbox bypass mechanism.
If you have questions or comments, please put them in a new thread. Place it in the Privacy & Security > General subtopic, and tag it with App Sandbox.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
[1] Most but not all. There are good reasons to sandbox your app even if you distribute it directly. See The Case for Sandboxing a Directly Distributed App.
Implementing Script Attachment in a Sandboxed App
Some apps support script attachment, that is, they allow a user to configure the app to run a script when a particular event occurs. For example:
A productivity app might let a user automate repetitive tasks by configuring a toolbar button to run a script.
A mail client might let a user add a script that processes incoming mail.
When adding script attachment to your app, consider whether your scripting mechanism is internal or external:
An internal script is one that only affects the state of the app.
A user script is one that operates as the user, that is, it can change the state of other apps or the system as a whole.
Supporting user scripts in a sandboxed app is a conundrum. The App Sandbox prevents your app from changing the state of other apps, but that’s exactly what your app needs to do to support user scripts.
NSUserScriptTask resolves this conundrum. Use it to run scripts that the user has placed in your app’s Script folder. Because these scripts were specifically installed by the user, their presence indicates user intent and the system runs them outside of your app’s sandbox.
Provide easy access to your app’s Script folder
Your application’s Scripts folder is hidden within ~/Library. To make it easier for the user to add scripts, add a button or menu item that uses NSWorkspace to show it in the Finder:
let scriptsDir = try FileManager.default.url(for: .applicationScriptsDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
NSWorkspace.shared.activateFileViewerSelecting([scriptsDir])
Enumerate the available scripts
To show a list of scripts to the user, enumerate the Scripts folder:
let scriptsDir = try FileManager.default.url(for: .applicationScriptsDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let scriptURLs = try FileManager.default.contentsOfDirectory(at: scriptsDir, includingPropertiesForKeys: [.localizedNameKey])
let scriptNames = try scriptURLs.map { url in
return try url.resourceValues(forKeys: [.localizedNameKey]).localizedName!
}
This uses .localizedNameKey to get the name to display to the user. This takes care of various edge cases, for example, it removes the file name extension if it’s hidden.
Run a script
To run a script, instantiate an NSUserScriptTask object and call its execute() method:
let script = try NSUserScriptTask(url: url)
try await script.execute()
Run a script with arguments
NSUserScriptTask has three subclasses that support additional functionality depending on the type of the script.
Use the NSUserUnixTask subsclass to run a Unix script and:
Supply command-line arguments.
Connect pipes to stdin, stdout, and stderr.
Get the termination status.
Use the NSUserAppleScriptTask subclass to run an AppleScript, executing either the run handler or a custom Apple event.
Use the NSUserAutomatorTask subclass to run an Automator workflow, supplying an optional input.
To determine what type of script you have, try casting it to each of the subclasses:
let script: NSUserScriptTask = …
switch script {
case let script as NSUserUnixTask:
… use Unix-specific functionality …
case let script as NSUserAppleScriptTask:
… use AppleScript-specific functionality …
case let script as NSUserAutomatorTask:
… use Automatic-specific functionality …
default:
… use generic functionality …
}