I'm working on a feature to enable user-provided scripts that will be executed by a Mac app.
NSUserScriptTask
underlies the script-calling code, and the NSUserAppleScriptTask
and NSUserAutomatorTask
subclasses both allow the setting of variables to pass information from Swift to the script:
Passing variables to an AppleScript
Setting NSUserAutomatorTask variables without requiring Automator Workflows to declare that variable
That leaves NSUserUnixTask
, which does not support setting variables. It instead supports a [String]
array called arguments
.
When executing the scripts, I will want to pass 3 variables from the Mac app:
let folderURL: String? = "/files/"
let fileURLs: [String] = ["/files/file1", "/files/file2"]
let selectionType: Int? = 1
let arguments: [String] = ["how", "should", "arguments", "be", "formatted", "?"]
let unixScript = try! NSUserUnixTask(url: url)
unixScript.execute(withArguments: arguments) { (error) in
if let error = error {
print(error)
}
}
The 3 swift variables must be condensed into a single [String]
array for NSUserUnixTask
to use as its arguments
parameter.
When the script runs, I want to then provide the script author access to the same arguments in a prototypical way:
#! /bin/bash
say "How should the script then access/parse the arguments?"
say $@ #says the arguments
Based on ease of use for the script author, how should the Swift code format its information into the arguments [String]
?
What boilerplate code could be provided to allow easy and pragmatic access to the parameters from the script?
There are many ways to do it, depending on what the shell script programmers expect. If I were one of them I'll ask you to keep it simple:
-flag value
)Following your example, slightly modified:
import Foundation
let shellScript = CommandLine.arguments[1]
let folderURL: String = "/files/"
let fileURLs: [String] = ["/files/file1", "/files/file2"]
let selectionType: Int? = 1
var arguments = [String]()
// script.sh <folder> [-type <type>] file1, file2, ...
arguments.append(folderURL) // <folder> is mandatory
if let type = selectionType { // -type <type> is optional
arguments.append("-type")
arguments.append("\(type)")
}
arguments += fileURLs // file1, ... (if it can't be empty check it here)
assert(FileManager.default.fileExists(atPath: shellScript))
let unixScript = try! NSUserUnixTask(url: URL(fileURLWithPath: shellScript))
let stdout = FileHandle.standardOutput
unixScript.standardOutput = stdout
unixScript.execute(withArguments: arguments) { error in
if let error = error {
print("Failed: ", error)
}
exit(0)
}
dispatchMain() // I'm not swift script expert, there may be a better way
And the corresponding shell script:
#!/bin/bash
# mandatory positional arguments
FOLDER=$1 && shift
# other mandatory arguments goes here
TYPE=7 # default type
FILES=()
while [[ $# -gt 0 ]]; do
arg=$1
case $arg in
-type)
TYPE=$2 && shift && shift
;;
# other named parameters here
*)
FILES+=($1) && shift
;;
esac
done
echo FOLDER: '<'${FOLDER}'>'
echo TYPE: '<'${TYPE}'>'
echo FILES: '<'${FILES[@]}'>'
exit 0
Example:
ScriptRunner /path/to/script.sh
The output:
FOLDER: </files/>
TYPE: <1>
FILES: </files/file1 /files/file2>
I used swift package manager to build:
// swift-tools-version:4.2
import PackageDescription
let package = Package(
name: "ScriptRunner",
dependencies: [],
targets: [
.target(name: "ScriptRunner", dependencies: [])
]
)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With