Framework Friday: OSAKit
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:
OSA Kit does have some downsides:
- As I mentioned, it isn’t documented. All we have to go on are the headers.
- There’s not even sample code, other than the example I wrote and included in this very post.
- 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.
November 9th, 2008 at 01:29:19
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…
November 9th, 2008 at 01:34:28
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.
November 9th, 2008 at 02:14:29
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.
November 9th, 2008 at 03:06:15
@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!
November 9th, 2008 at 03:39:33
Carl: you can use appscript to do AppleScript from python (http://appscript.sourceforge.net/)
November 9th, 2008 at 08:58:08
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. :-)
November 9th, 2008 at 10:33:48
@Carl:
You can also use ScriptingBridge.framework from python. http://developer.apple.com/documentation/Cocoa/Conceptual/ScriptingBridgeConcepts/Introduction/chapter_1_section_1.html
November 9th, 2008 at 11:48:29
-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.
November 9th, 2008 at 13:21:18
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.)
November 9th, 2008 at 17:04:00
Thanks for the post, Peter. Getting a 404 on the download link for the OSAKit-osascript-1.0.1.tbz project, though.
November 9th, 2008 at 17:28:15
Jon Nathan: Sorry; I broke the link in changing it to the new filename.
I have now fixed it. Thanks for the report.
November 9th, 2008 at 22:54:12
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
November 10th, 2008 at 00:03:05
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.
November 10th, 2008 at 09:30:45
Heh, not sure what you mean by no. No it didn’t work, or no you refuse to follow my advice?
November 10th, 2008 at 09:51:43
Jeff: No, your statements are wrong.
to handler-name
is a valid way to start a handler. It’s synonymous withon handler-name
.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.run
. For therun
handler, the parentheses are optional.I sincerely thank you for trying, but none of the advice you gave applies.
November 10th, 2008 at 11:33:17
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.
November 10th, 2008 at 11:50:26
Actually, scratch that. It works even without conversion. But in any case, it does work on handlers besides the “run” one.
November 10th, 2008 at 14:14:20
-[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
November 15th, 2008 at 07:08:59
Can OSAKit be used on a secondary thread? That’s the primary limitation of NSAppleScript for me, currently.
November 15th, 2008 at 14:15:36
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. ☺
November 17th, 2008 at 10:37:30
Bug filed :-)
November 17th, 2008 at 14:30:42
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.