8

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?

pkamb
  • 33,281
  • 23
  • 160
  • 191
  • @Inian I will be providing the arguments that a developer would need to launch their script, passing the needed parameters from the Swift code. I need to format said arguments + any flags, etc., in a way that would be useful for script developers. I am unsure what that format should look like. That's how the bash scripting tag ties in here. Would like your recommendation if you have any. – pkamb Oct 02 '18 at 06:26
  • Could you also add some real values for such args and flags and let know how do you want `bash` to parse them. This information is a bit too vague to provide a meaningful answer. I could really help with such more examples – Inian Oct 03 '18 at 04:04
  • @Inian that's really the question I'm asking: I do not know what flags I should provide or how the parameters should be structured or parsed. I have the the Swift arguments as shown in the question. I would like to provide those arguments to the script developer in an idiomatic format. – pkamb Oct 03 '18 at 04:52
  • When you mean _to the script developer_, so the arguments from the swift code will be processed by a `bash` script? Have you achieved in _some_ way to let it pass to this `bash` script? If _not_ could you try in some way and show how it is received in the `bash` script? – Inian Oct 03 '18 at 07:31
  • If you can let know some incorrect way of how the arguments are passed _currently_ to the `bash` script, I can provide improvements or suggestions to it – Inian Oct 03 '18 at 07:42
  • @Inian the current code is shown in the question. I want to pass the `folderURL: String`, `fileURLs: [String]`, and `selectionType: Int` information to a bash script via the `arguments: [String]` parameter. I want the script to then access and print out the values of the information. Just a very basic script, as sample code for future devs. – pkamb Oct 03 '18 at 16:28
  • So correct me if I'm wrong here. So the `bash` script receives arguments `folderURL: `, `fileURLs: `, and `selectionType: `. Let us assume these are arguments 1 to 3. Since these are strings, what are they de-limited on? How would the bash script distinguish argument 1 fro 2 and subsequently? – Inian Oct 03 '18 at 17:15
  • Also I've asked a question before. You've used `say` in the `bash` script with she-bang set to `#!/bin/bash`, which means only the built-ins from the shell are _only_ supported. Did you mean `osascript -e 'say'`? – Inian Oct 03 '18 at 17:18
  • @Inian *That's exactly the question I'm asking!* I do not know. I want to write this in a way that is idiomatic for script developers, using standard flags or de-limiting schemes, etc. But I do not have enough scripting knowledge to know what that idiomatic scheme would look like. – pkamb Oct 03 '18 at 17:20
  • @Inian re: `say`: the script shown in the question is not important; it was just a sample that outputted the arguments *on my machine*. Something similar that prints out the arguments or whatever would be fine. – pkamb Oct 03 '18 at 17:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/181240/discussion-between-inian-and-pkamb). – Inian Oct 03 '18 at 17:24

1 Answers1

4

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:

  • position parameters (fixed number of mandatory parameters at the start)
  • optional named parameters (any parameter of the style -flag value)
  • more parameters (a variable number of additional parameters)

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: [])
    ]
)
djromero
  • 19,551
  • 4
  • 71
  • 68
  • This is perfect, thank you for the detailed answer. The switch for assigning the arguments is great. – pkamb Oct 24 '18 at 05:50