1

I am trying to make a program in Swift 2 that runs and gets the result of an AppleScript script.

Here is my code:

import Foundation

func runAppleScript(script:String) -> String
{
    let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>()
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
    let theResult:String = theDiscriptor.stringValue! //This is whats causing the error

    return theResult
}

let scriptResult = runAppleScript("tell app \"Spotify\" to playpause")

NSLog("\(scriptResult)")

The problem is the program crashes and outputs:

fatal error: unexpectedly found nil while unwrapping an Optional value

in the console. I have also tried if let else, however that does not work either. How would I fix this issue?

This was tested using a OS X Command Line template using the swift language.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
iProgram
  • 6,057
  • 9
  • 39
  • 80
  • What line is it crashing on? I could see the nslog crashing if there was no scriptResult – bolnad Oct 29 '15 at 22:09
  • @bolnad Think it was the `let` line. Will check tomorrow. (GMT) Don't have access to my computer as of now. – iProgram Oct 29 '15 at 22:38
  • @bolnad The error is from the line `let theResult:String = theDiscriptor.stringValue!` – iProgram Oct 30 '15 at 11:01
  • Your using the ! And that's telling it not to bother unwrapping as you know it won't be empty so if you wrap it in the if let theResult = theDiscripter.stringValue { } then it won't crash but it won't solve your issue of it being empty – bolnad Oct 30 '15 at 11:21
  • @bolnad Just figured that out myself. :) – iProgram Oct 30 '15 at 11:28
  • Does `playpause` return a string at all? – vadian Jul 13 '16 at 12:44
  • @vadian, I have updated the fixed issue in my answer. By doing that, it must have pushed this question to the top where everyone could see it. Sorry about that. Also no it does not. – iProgram Jul 13 '16 at 12:50
  • @iProgram Then it's completely useless to handle any return value (aside from the error dictionary). – vadian Jul 13 '16 at 17:58
  • @vadian Thats because I need to use other scripts too. I just wanted to use a script that didn't return to make sure it worked first. – iProgram Jul 14 '16 at 10:02
  • @iProgram To be more generic return the `AppleEventDescriptor` which is `nil` in case of no return value. Your code considers only scripts which return a string value. – vadian Jul 14 '16 at 10:31

2 Answers2

1

Actually the error could come from NSAppleScript(source: script)! so the proper solution is to return an Optional String and not use force unwrapping at all:

func runAppleScript(script:String) -> String? {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo)
    return theDescriptor?.stringValue
}

if let scriptResult = runAppleScript("tell app \"Spotify\" to playpause") {
    NSLog("\(scriptResult)")
} else {
    print("the script execution failed")
}

If you prefer having a default value instead of nil when it fails, then no need to return an Optional:

func runAppleScript(script:String) -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo)
    return theDescriptor?.stringValue ?? ""  // if nil, returns the default ""
}

let scriptResult = runAppleScript("tell app \"Spotify\" to playpause")
NSLog("\(scriptResult)")

As for using the new Swift 2 error handling system, none of the methods you're using inside runAppleScript are throwing errors, so it would only work if you used a custom error type and throw the errors yourself. Example:

enum MyAppleScriptError: ErrorType {
    case ExecutingScriptFailed
    case GettingStringValueFailed
}

func runAppleScript(script:String) throws -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?> = nil
    let startAtLoginScript = NSAppleScript(source: script)
    guard let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo) else {
        throw MyAppleScriptError.ExecutingScriptFailed
    }
    guard let value = theDescriptor.stringValue else {
        throw MyAppleScriptError.GettingStringValueFailed
    }
    return value
}

do {
    let scriptResult = try runAppleScript("tell app \"Spotify\" to playpause")
    NSLog("\(scriptResult)")
} catch {
    print(error)
}

Swift 3

Same idea, but some implementation details are different.

func runAppleScript(_ script:String) -> String? {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?>? = nil
    if let startAtLoginScript = NSAppleScript(source: script) {
        let theDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
        return theDescriptor.stringValue
    }
    return nil
}

if let scriptResult = runAppleScript("tell app \"Spotify\" to playpause") {
    NSLog("\(scriptResult)")
} else {
    print("no return value")
}

And with error handling:

enum MyAppleScriptError: ErrorProtocol {
    case ExecutingScriptFailed
    case GettingStringValueFailed
}

func runAppleScript(_ script:String) throws -> String {
    let errorInfo: AutoreleasingUnsafeMutablePointer<NSDictionary?>? = nil
    let startAtLoginScript = NSAppleScript(source: script)
    guard let theDescriptor = startAtLoginScript?.executeAndReturnError(errorInfo) else {
        throw MyAppleScriptError.ExecutingScriptFailed
    }
    guard let value = theDescriptor.stringValue else {
        throw MyAppleScriptError.GettingStringValueFailed
    }
    return value
}

do {
    let scriptResult = try runAppleScript("tell app \"Spotify\" to playpause")
    NSLog("\(scriptResult)")
} catch {
    print(error)
}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
  • 1
    Thats a good answer, however as you see, I have answered it my self (just before you did) and I have the check in the function. This will help to make my code neater later on since I will be running a lot of AppleScripts. I shall still mark it as the correct answer though. – iProgram Oct 30 '15 at 11:31
  • Thank you. I didn't notice you made an answer while I was making mine, then when I saw it I thought mine was still relevant because it's a bit safer (although yours was ok too). – Eric Aya Oct 30 '15 at 11:34
  • Your welcome. I may also try making my function to throw an error. This way I can do `let scriptResult = try runAppleScript ?? ""` if it's possible? – iProgram Oct 30 '15 at 12:03
  • Where is the variable `error` coming from? Also how would I print one error if the script failed to execute, and print another error if it failed to get the return value? – iProgram Oct 30 '15 at 12:32
  • 1
    This `error` variable is automatically generated by the `catch` instruction. // Look at my example: we throw a different error for each case, so the `print(error)` will reflect which one failed. :) – Eric Aya Oct 30 '15 at 12:34
  • Just tried to do `try runAppleScript(scripts.playerState)`, however in swift 3, it some reason says 'extra argument in call' – iProgram Jul 13 '16 at 17:00
  • 1
    Ah, true, this answer needs some work to be compatible with Swift 3. I'll update as soon as possible. – Eric Aya Jul 13 '16 at 17:01
  • Updated. Don't hesitate to tell me if something is not clear. – Eric Aya Jul 13 '16 at 17:57
0

I have fixed my own code.

import Foundation

func runAppleScript(script:String) -> String
{
    let theResult:String
    let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>()
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(errorInfo)
    if let _ = theDiscriptor.stringValue
    {
        theResult = theDiscriptor.stringValue!
    } else {
        theResult = ""
    }

    return theResult
}



let scriptResult = runAppleScript("")

What I had to do was check if theDiscriptor.stringValue has a value before unwrapping it. The error that I was getting is because I was trying to check the value after I had unwrapped it. Simply removing the ! on the check fixed my problem.

Edit

When trying this in Swift 3, the code let errorInfo = AutoreleasingUnsafeMutablePointer<NSDictionary?>() no longer works. To fix this, I have updated the code.

func runAppleScript(script:String) -> String?
{
    var theResult:String?
    let startAtLoginScript: NSAppleScript = NSAppleScript(source: script)!
    var errorInfo:NSDictionary? = nil
    let theDiscriptor:NSAppleEventDescriptor = startAtLoginScript.executeAndReturnError(&errorInfo)
    if let _ = theDiscriptor.stringValue {theResult = theDiscriptor.stringValue!}
    return theResult
}

Bonus

By returning an optional string, it allows you to check if the code returned a value.

Example:

Old way

let output = runAppleScript("script")
if output != ""
{
    //Script returned date
} else {
    //Script did not return data
}

New way

if let output = runAppleScript("script")
{
    //Script returned data
} else {
    //Script did not return data
}
iProgram
  • 6,057
  • 9
  • 39
  • 80
  • @EricD Didn't realise it was not safe. I just thought it was better because it had less lines of code. Let me check if it works on swift 3. If so I shall reaccept your code – iProgram Jul 13 '16 at 16:54
  • @EricD Thats the reason why I re-looked at your answer because of safety. – iProgram Jul 13 '16 at 17:01