tell application “YourGreatApp” NSAppleScript is here to save the day

Cocoa includes the wonderful NSAppleScript class, which lets you embed a high-level AppleScript code snippets in your Objective-C program. AppleScript is the bee’s knees for interfacing with other applications, particularly Apple programs and OS X itself. EDITED TO ADD: Unless you use threads in your program. Here is a more sobering perspective, written several months after this article.

For example, say you want to get the user’s current status message from iChat. You can mess around with the InstantMessaging.framework — only to discover that it can’t do this — or you can wrap the following AppleScript:

tell application “iChat” to get status message

in an NSAppleScript object, like so:
NSAppleScript *iChatGetStatusScript; //this would be in the header.
/*Apple recommends only compiling NSAppleScript objects once, for performance reasons. Be sure to call [iChatGetStatusScript release] in your dealloc method.*/
if(!iChatGetStatusScript)
  iChatGetStatusScript = [[NSAppleScript alloc] initWithSource:
    @”tell application \”iChat\” to get status message”
];
NSString *statusString =
[[iChatGetStatusScript executeAndReturnError:&errorDict] stringValue];

Skeptical readers have probably noticed that I cheated by picking functionality that wasn’t in a framework for the last example. Unfortunately this situation is more of a rule then an exception. AppleEvents have been on the Apple software development scene longer then Cocoa. Lots of programs only expose functionality through AppleScript/AppleEvents. Using NSAppleScript is the best way to interact with these programs from Cocoa.

AppleScript is a big part of how FrontRow integrates with iTunes, even though they are both modern programs.
grok the ouptut of:
strings /System/Library/CoreServices/Front\ Row.app/Contents/MacOS/Front\ Row
(hint: look for “tell”), and you’ll see a lot of gems like:
tell application “iTunes” to get (data of artwork 1 of current track) as picture
Or:
tell application “Finder”
    set myCD to first disk whose format is audio format

Now I am the first to admit that a well-written Objective-C library trumps duck-taping one language on-top of another. But we don’t have the luxury of always being given well-written Objective-C interfaces.

As of version 0.24 IMLocation uses AppleScript to: interface with iChat and Adium; install/uninstall itself; mute/unmute sound; perform inter-process communication between the helper and GUI; and to get the MAC address of the first router connected to the ethernet port. NSAppleScript has also been heavily used during development for prototyping.

EDITED TO ADD: NSAppleScript is pathologically un-thread-safe. That is to say, you can only use it on from the main thread. No amount of @synchronized blocks will keep you from crashing if you use NSAppleScript in any other way. There is a workaround, but the cure is worse then the disease. You can use an NSTask to have the osascript program interpret the script for you. This won’t block your thread, but it will be take a long time, and is resource un-friendly.

NSTasks are the way to call the command line, or embed snippets of a different language in cocoa. I was originally using the do shell command AppleScript construct. If you are using it, please switch to NSTasks. They are much more powerful, still easy to use, and prevent all sorts of bugs caused by quote-marks in AppleScript.

Advertisements
Explore posts in the same categories: AppleScript, Cocoa, MacOSX, NSAppleScript, NSTask, Objective-C, Programming

9 Comments on “tell application “YourGreatApp” NSAppleScript is here to save the day”

  1. imlocation Says:

    Sorry about the blue AppleScript code-snippits, I know they look just like links. Unfortunately that’s how ScriptEditor colors AppleScript code by default.

  2. has Says:

    “””Now I am the first to admit that a well-written Objective-C library trumps duck-taping one language on-top of another.”””

    If you’re interested, there’s a third-party Apple event bridge currently in development for ObjC, allowing you to skip AppleScript and talk to applications directly:

    http://appscript.sourceforge.net/objc-appscript.html

    e.g. To get iChat’s status message:


    // Generate glue: osaglue IC iChat
    OSErr err = noErr;
    ICApplication *iChat = [[ICApplication alloc] initWithName: @"iChat.app"];
    ICGetCommand *getStatusCmd = [[iChat statusMessage] get];
    NSString *statusMessage = [getStatusCmd send];
    if (statusMessage == nil) err = [getStatusCmd errorNumber];

    HTH

  3. imlocation Says:

    More and more AppleScript code in IMLocation is getting replaced, because it is not powerful enough, clumsy, or not thread-safe.

    I’ve stopped using AppleScript to communicate with Adium — instead I am writing an Adium plugin. I’m also using a private/unsupported interface to interface with iChat, because it is much more powerful then AppleScript.

  4. Elise van Looij Says:

    Being an Applescripter moving into Cocoa programming, I confess I know very little of threads, be they safe or not. However, experience have taught me that using the various plugins and bridges that people cobble together, is definitely not the way to go. These plugins are rarely well thought out, often return unexpected results in strange formats, and are seldom supported, let alone upgraded. Applescript, on the other hand, is actively supported by Apple and various other parties and has improved dramatically over the past two years. So, if you want your Cocoa app to talk to other apps, Applescript is the way to go.
    For the best way to acoomplish that, check out Apple’s Technical note TN2084 and Applescript and Cocoa. One tip you’ll read there is that it’s better to save your applescripts as scripts (add an Applescript Script File in XCode) and then call the compiled script in Cocoa.

  5. Elise van Looij Says:

    BTW, Technote TN2084 describes adding a Run Script Build Phase to XCode, which is no longer necessary. Just add the Compile Applescripts Build Phase and all your applescript text files will be compilod to script files.

    Also, the method described above for reading out the NSAppleEventDescriptor is a bit more complicated than is necessary most of the time. If you’re expecting a single string, just use stringValue. So my version would be:

    In an applescript text file called getMessage.applescript:

    tell application ”iChat”
    get status message
    end tell

    In a Cocoa / Objective-C class:

    NSString *scriptPath = [[NSBundle mainBundle] pathForResource: @”getMessage” ofType: @”scpt” inDirectory: @”Scripts”];
    //It doesn’t matter in which group in XCode you put getMessage.applescript, it will be compiled to Scripts/ getMessage.scpt in the bundle
    NSDictionary *errorDict;
    NSAppleScript *theScript = [[NSAppleScript alloc] initWithContentsOfURL: [NSURL fileURLWithPath: scriptPath] error: nil];
    NSAppleEventDescriptor *resultDescriptor = [theScript executeAndReturnError:&errorDict];
    NSLog([resultDescriptor stringValue]);

  6. imlocation Says:

    Elise,

    Thanks for the reply, I have updated the article with your recommendations. Also, the 3-line AppleScript I was using was unnecessarily long, this has been corrected.

    Putting a large AppleScript in a separate source-file makes sense. But in my opinion, for 1-5 line snippets, embedding the AppleScript right in the coca source code is the best way to go. It’s clear exactly what the code does, because it’s all right there — no cross-referencing in separate .scpt files. Executing a .scpt from your application’s bundle is conceptually no different from executing a perl script, or any other type of script for that matter. The beauty of NSAppleScript is that it lets you put AppleScript code right in your Cocoa code, which is very useful. I know Apple does this, because the FrontRow executable has strings of AppleScript code inside it.

    What you say about plugins and threads seems pretty close to what I was feeling when I first wrote this article. AppleScript interfaces do seem to be very stable and a lot of them have been around longer then Coco interfaces. But since then, few things have happened, and all of them have reduced the amount of AppleScript code I use.

    I begun using threads more heavily, which made using NSAppleScript much more of a hassle. Also, code that used to work, and was thread-safe except for NSAppleScript was suddenly broken. This made me wary of using NSAppleScript in the future. Hopefully things will be thread-safe in the future, but I’m not holding my breath.

    I learned about NSTasks, and have more experience with Cooca. This has made other ways of getting things done, including buggy plugins + reverse-engineered interfaces, a bit more manageable for me.

    I want to do things with a program that there is simply no way to do with a program’s AppleScript interface.


  7. […] The NSAppleScript honeymoon is over. I don’t like AppleScript, I don’t like […]


  8. […] But it gets better. We can remove the run and try blocks as well, and just ignore any errors the interpreter complains about. That makes the script one line (ok, it’s wrapping in this poorly laid-out blog, but it’s still one line, down from 12 lines. Now it’s short enough to embed directly in Cocoa code with NSAppleScript. […]


  9. […] hacks I’m about to discuss, check to see if existing frameworks , or iChat’s AppleScript interface, will do what you need. Any software update can break unsupported code in unpredictable ways at any […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: