Framework Friday: OSAKit

2008-11-09 00:57:12 UTC

Apologies for posting this late. I was finishing up the test app and filing relevant bugs.


So, how do you run an AppleScript script?

One way is NSAppleScript. The problem with that is that, in our experience on the Adium project, it leaks memory. Profusely. (Maybe this has changed; Adium has for a long time used a separate process for running AppleScript scripts.)

Another way is to use the Open Scripting Architecture API directly. But that API is pre-Carbon, making it painful to deal with. One specific problem is that the object types aren’t reference-counted.

The third way is OSAScript, one of the classes in OSA Kit.

Wait. OSA Kit?

Yes. OSA Kit is an Objective-C wrapper around the Open Scripting Architecture. Like QTKit (a similar wrapper around QuickTime), it’s a framework in Mac OS X that Apple added in version 10.4. It’s public, but undocumented: As of right now, searching developer.apple.com for that name yields exactly five hits, none of which are documentation of OSA Kit.

One of those hits is the list of system frameworks, which says that OSA Kit:

Contains Objective-C interfaces for managing and executing OSA-compliant scripts from your Cocoa applications.

OSA-compliant… like AppleScript!

The interface for OSAScript is the same as that of NSAppleScript, except larger: OSAScript is a superset of NSAppleScript. This is mostly because the OSA supports languages other than AppleScript, such as JavaScript (with this component). Running a script, for example, is no different:

NSDictionary *errorDict = nil;
script = [[[(OSAScript | NSAppleScript) alloc] initWithContentsOfURL:scriptURL error:&errorDict] autorelease];
result = [script executeAndReturnError:&errorDict];

But, unlike NSAppleScript, the OSA Kit does not stop there.

OSA Kit is to Script Editor as Image Kit and PDF Kit are to Preview. Apple removed those applications’ capabilities to a new framework, and rewrote the applications to use the new frameworks. In the process, they added features and made the existing features prettier.

In the case of OSA Kit, the new framework includes classes you can use to build your own Script Editor (or CLImax)—specifically, OSAScriptView and OSAScriptController. You may find these classes useful if you plan to embed some of Script Editor’s functionality into your application.

Of course, you don’t need to use OSA Kit from an application. Here’s a command-line tool I wrote:

File: OSAKit-osascript-1.0.2.tbz

A clone of osascript, written to use either NSAppleScript or OSAScript. The Xcode project contains two targets, one for each version.

There are very few differences between the two variants:

  1. Obviously, the OSA Kit version uses OSAScript, whereas the other version uses NSAppleScript.
  2. The OSA Kit version has an option to allow you to specify the language, and another one to list available languages. (NSAppleScript, as you might guess, only supports AppleScript.)
  3. The OSA Kit version can (theoretically) pass arguments to the script; NSAppleScript doesn’t support this.

OSA Kit does have some downsides:

  1. As I mentioned, it isn’t documented. All we have to go on are the headers.
  2. There’s not even sample code, other than the example I wrote and included in this very post.
  3. Calling a handler by name with arguments doesn’t work (hence the “theoretically” above). I believe that this is a bug in OSAScript; I have filed it, with a summary of -[OSAScript executeHandlerWithName:arguments:error:] always returns an error, never executes the handler.

There is also one potential downside: I don’t know for certain that OSAScript doesn’t leak memory the same way NSAppleScript does (or did). It’s worth trying, though, and it’s more capable anyway.

I consider the OSA Kit to be the future of running AppleScript scripts from Objective-C.

UPDATE 2008-11-09 13:18 PST: Replaced OSAKit osascript 1.0 with 1.0.1, which has a couple more sample scripts.

UPDATE 2008-11-09 17:40 PST: Changed link to CLImax. I had linked to Drew Thaler’s blog post announcing its re-release; now I’m linking to the actual product page, as saved by the Internet Archive’s Wayback Machine.

UPDATE 2008-11-11 20:43 PST: Replaced OSAKit osascript 1.0.1 with 1.0.2, which has better (more GCC-like) error-reporting.

22 Responses to “Framework Friday: OSAKit”

  1. Carl Says:

    OT, but I know you’re also into Python. Is there a good way to do AppleScript from Python? I’ve looked at the Apple website before and they make it seem like there might be, but then do absolutely zero documentation of what it is…

  2. Carl Says:

    Oh, did you see how your proposed for-loop chainer is in itertools for Python 2.6:


    >>> from itertools import product
    >>> tf = (True, False)
    >>> for x, y, z in product(tf, tf, tf):
    ... print(x, y, z)
    ...
    True True True
    True True False
    True False True
    True False False
    False True True
    False True False
    False False True
    False False False

    Pretty cool.

  3. Peter Hosey Says:

    Carl:

    #1: Not that I know of. import Carbon.OSA doesn’t work, and PyObjC doesn’t expose the OSA Kit.

    #2: You’re referring to this previous blog post, I think. I hadn’t seen that. Awesome.

  4. Martin Michel Says:

    @Carl:
    If you want to do AppleScript from Python, I absolutely recommend to take a look at py-appscript developed by Hamish Sanderson:

    http://appscript.sourceforge.net/

    It’s really a great tool and I use it all the time for my private scripting projects. Of course you can also combine AppleScript & Python with AppleScript’s «do shell script» command. I have written a small article about this at MacScripter:

    http://bbs.macscripter.net/viewtopic.php?pid=98398

    @Peter:
    Thank you so much for this article. I was always unhappy with NSAppleScript and will now try to use the OSAScript class for some of my command line tools.

    Best regards from Germany!

  5. Ronald Oussoren Says:

    Carl: you can use appscript to do AppleScript from python (http://appscript.sourceforge.net/)

  6. Jeff Johnson Says:

    Peter, what exactly is your experience with -[OSAScript executeHandlerWithName:arguments:error:]? Does it always fail when you send arguments, or does it always fail period? I definitely have it working with an empty array for arguments, and I seem to recall getting it to work with arguments, though I’m not certain about the latter. I may be able to help if I can see the script you’re using. It’s extremely finicky, just like Applescript itself. :-)

  7. Harvey Swik Says:

    @Carl:
    You can also use ScriptingBridge.framework from python. http://developer.apple.com/documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/Introduction/chapter_1_section_1.html

  8. Paul Kim Says:

    -executeHandlerWithName:arguments:error: works fine for me. You just have to make sure that you lowercase the handler name (even if the actual handler name is mixed/upper case). This was also the case in the different categories people did to extend NSAppleScript to be able to call handler with arguments.

  9. Peter Hosey Says:

    Jeff and Paul: Download the test app. I’ve updated it to include a couple more sample scripts.

    It definitely does not work with the run handler. Calling OSAGetHandlerNames shows the run handler as an Apple Event descriptor rather than a string descriptor, so that may be the problem there.

    But I couldn’t get it to work with a specific handler, either—I got the same result either way. (Yes, I did add a message to call that handler specifically.)

  10. Jon Nathan Says:

    Thanks for the post, Peter. Getting a 404 on the download link for the OSAKit-osascript-1.0.1.tbz project, though.

  11. Peter Hosey Says:

    Jon Nathan: Sorry; I broke the link in changing it to the new filename.

    I have now fixed it. Thanks for the report.

  12. Jeff Johnson Says:

    Several issues: (1) The syntax is “on” rather than “to” for the handlers. (2) I think that “run” may be a reserved word, try “my_run”. (3) You need parentheses after the handler name, e.g., “my_run()”. (4) It doesn’t seem to like the .applescript file extension, try .scpt.


    on my_run()
    beep
    end my_run

  13. Peter Hosey Says:

    Jeff: No. The script is a valid AppleScript script (1–3), and both my application and OSA support .applescript files just fine (4). In fact, it shouldn’t make any difference whether it’s already-compiled or not, since OSA has supported plain-text scripts since Mac OS.

  14. Jeff Johnson Says:

    Heh, not sure what you mean by no. No it didn’t work, or no you refuse to follow my advice?

  15. Peter Hosey Says:

    Jeff: No, your statements are wrong.

    1. to handler-name is a valid way to start a handler. It’s synonymous with on handler-name.
    2. run is a reserved word: reserved for the name of the main handler (synonymous with just putting all your code at the top level, if you don’t have any other handlers). That’s what I wanted.
    3. You only need parentheses after the handler name if the handler is not run. For the run handler, the parentheses are optional.
    4. OSA, OSA Kit, and my program all accept text-only (.applescript) scripts, and treat them equally to how they treat compiled (.scpt) scripts. (How to prove this: Compile the script, then run my program on the result. You’ll see the same result.)

    I sincerely thank you for trying, but none of the advice you gave applies.

  16. Paul Kim Says:

    I did manage to get your program working when using a non-“run” handler. One problem I found was that the args aren’t being converted to NSAppleEventDescriptors. Try that and see if it works for you.

  17. Paul Kim Says:

    Actually, scratch that. It works even without conversion. But in any case, it does work on handlers besides the “run” one.

  18. has Says:

    -[OSAScript executeHandlerWithName:arguments:error:] is working as advertised; the problem is that it’s only suitable for calling AppleScript handlers whose names are user-defined identifiers. Handlers whose names are reserved keywords must be invoked using the equivalent raw AE codes. Here’s a simple example to illustrate (it’s modified from the CallAppleScriptHandler project in the objc-appscript svn repository):


    #import "Appscript/Appscript.h"

    int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    // Create AEMApplication object for building AEMEvents
    AEMApplication *app = [[AEMApplication alloc] init];

    // Create AEMCodecs object for unpacking script results
    AEMCodecs *codecs = [[AEMCodecs alloc] init];

    // Create 'aevtoapp' (run) AEMEvent
    AEMEvent *evt = [app eventWithEventClass:kCoreEventClass
    eventID:kAEOpenApplication];

    // Build direct parameter for 'run' event and add it to AEMEvent
    NSMutableArray *params = [NSMutableArray array];
    int i;
    for (i=1; i < argc; i++)
    [params addObject: [NSString stringWithUTF8String: argv[i]]];
    [evt setParameter:params forKeyword:keyDirectObject];

    // Compile (or load) the AppleScript
    NSAppleScript *scpt = [[NSAppleScript alloc] initWithSource:
    @"on run params\n"
    @" return params as text\n"
    @"end run"];

    // Get an NSAppleEventDescriptor from AEMEvent
    NSAppleEventDescriptor *evtDesc = [evt descriptor];

    // Send event to AppleScript
    NSDictionary *error = nil;
    NSAppleEventDescriptor *resultDesc = [scpt executeAppleEvent:evtDesc
    error:&error];

    if (resultDesc) {
    // Unpack script result
    id res = [codecs unpack:resultDesc];
    NSLog(@"Result = %@", res);
    } else
    NSLog(@"Script error = %@", error);

    [scpt release];
    [codecs release];
    [app release];
    [pool release];
    return 0;
    }

    Uses objc-appscript’s AEM API for building events and converting ObjC values to NSAppleEventDescriptors and back. Plus NSAppleScript (which does indeed leak like a sieve on 10.4 and earlier, although I believe it’s somewhat improved in 10.5) for executing scripts, but you could replace that with OSAScript easily enough if you wanted.

    HTH

    has

    Control AppleScriptable applications from Python, Ruby and ObjC:
    http://appscript.sourceforge.net

  19. Joshua Says:

    Can OSAKit be used on a secondary thread? That’s the primary limitation of NSAppleScript for me, currently.

  20. Peter Hosey Says:

    Joshua: I don’t know anything that isn’t in the headers or the documentation. Neither of those said anything about it. (Of course, there is no documentation specifically for OSA Kit; the only material about OSA Kit in the documentation is that which mentions OSA Kit.)

    So, you should file a documentation bug. ☺

  21. Joshua Says:

    Bug filed :-)

  22. Peter Hosey Says:

    Joshua: Don’t forget to link to the x-radar://problem/XXXXXX URL, so that any Apple employees who read this post can get to your bug easily.

    If possible, it would also be nice if you’d copy the report into OpenRadar, so that anyone can read it.

Leave a Reply

Do not delete the second sentence.