Hello fellow Mac rubyists and AppleScript haters,
For those of you that have experience with both rubyosa and rb-appscript, I'd like the hear the pros and cons of each, which one you decided to stick with, and which one you'd recommend for a totally non-AppleScript savvy ruby old-timer. Also, are there any other options that I have missed?
As an aside, any tips dealing with the AppleScript side of the equation (e.g. browsing dictionaries, etc.) are also welcome.
Seeing some sample code also helps a lot.
Quoth kch:
That's nice, but now I'm curious about how scripting bridge compares to applescript. I guess I'll have some reading to do.
SB omits some functionality found in AppleScript. For example, the following script moves all files from the desktop to the Documents folder:
tell application "Finder"
move every file of desktop to folder "Documents" of home
end tell
In SB, the SBElementArray class severely restricts your ability to apply a single command to multiple objects, so you either have to resort to the low-level API or else get a list of individual file references and move them one at a time:
require 'osx/cocoa'; include OSX
require_framework 'ScriptingBridge'
finder = SBApplication.applicationWithBundleIdentifier('com.apple.finder')
destination = finder.home.folders.objectWithName('Documents')
finder.desktop.files.get.each do |f|
f.moveTo_replacing_positionedAt_routingSuppressed(destination, nil, nil, nil)
end
In rb-appscript, you'd use the same approach as AppleScript:
require 'appscript'; include Appscript
app("Finder").desktop.files.move(:to => app.home.folders["Documents"])
...
SB obfuscates the Apple event mechanism much more heavily than AppleScript does. AppleScript can be a pain to get your head around, what with the weird syntax, tendency to keyword conflicts, and the like, but beyond that it largely presents Apple events as-is. The only really significant piece of magic in AS is its 'implicit get' behaviour when it evaluates a literal reference that doesn't appear as a parameter to a command. AppleScript's biggest sin is that its documentation doesn't better explain how it actually works, but there is a very good paper by William Cook that sheds a lot of light on what's actually going on.
SB, on the other hand, does its hardest to pretend that it is a genuine Cocoa API with Cocoa-style behaviour, so layers on a large amount of magic. The result is something superficially appealing to Cocoa developers, but as soon as those abstractions start to leak - as abstractions invariably do - you are completely at sea in terms of understanding what's going on. For example, SBElementArray claims to be an array - it even subclasses NSMutableArray - but when you actually try to use its array methods, half of them work and half of them don't. In fact, it isn't a real array at all; it's a wrapper around an unevaluated Apple event object specifier, faked up to pretend it's an NSMutableArray. So when it does something un-array-like, you're largely stuffed for understanding why. And, as mentioned in #1, some of these thick abstractions make it difficult to access standard Apple event functionality underneath.
SB firstmost tries to be a good Cocoa API rather than a good Apple event API, and ends up being not very good at either.
Appscript, incidentally, follows AppleScript's lead and takes the opposite approach: do Apple events right, and then worry about accommodating the host language. That's why some folks prefer RubyOSA over rb-appscript; while appscript is the more capable solution, if you've coming from a heavily object-oriented background, it will feel very strange. That's because Apple events use an RPC-plus-query-based paradigm, and any resemblance appscript may have to OOP is purely syntactic. The nearest analogy would be to sending XQueries over XML-RPC, and it takes some getting used to.
...
SB tends to suffer significantly more application compatibility problems than AppleScript.
Some of these problems are due to SB imposing its own ideas of how Apple event IPC ought to work on top of how it actually works. For example, SB creates a set of [pseudo] proxy classes representing the classes defined in the dictionary; it then imposes various restrictions on how you can interact with those objects based largely on classic object-oriented behavioural rules.
For example, the following script gets the names of all sub-folders of the Documents folder:
tell application "Finder"
get name of every folder of entire contents of folder "Documents" of home
end tell
If you try the same approach in SB:
finder.home.folders.objectWithName('Documents').entireContents.folders.arrayByApplyingSelector(:name)
it gets as far as the #folders method, then throws an error because the type of the 'entire contents' property in Finder's dictionary is declared as 'reference'. Since there isn't a 'reference' class with 'folder' elements defined in the dictionary, SB doesn't let you construct that particular query (unless you want to drop down to the low-level APIs and use raw AE codes). It's perfectly legal according to Apple event rules, but doesn't fit within the narrower OO-centric rule set imposed by SB.
Other bugs are due to SB making assumptions about how scriptable applications will implement certain commands and other features. For example:
tell application "iTunes"
make new playlist with properties {name:"test 1"}
end tell
SB doesn't let you take advantage of any shortcuts provided by iTunes though (you can omit the reference to the source object you want the playlist created in, in which case the main 'Library' source is used), so let's write that in full for a better comparison:
tell application "iTunes"
make new playlist at source "Library" with properties {name:"test"}
end tell
In SB you'd write this as:
itunes = SBApplication.applicationWithBundleIdentifier('com.apple.itunes')
playlists = itunes.sources.objectAtIndex(0).playlists()
newplaylist = itunes.classForScriptingClass(:playlist).alloc().initWithProperties({:name => 'test'})
playlists.addObject(newplaylist)
When you run it though, it barfs on #addObject. In its attempt to turn a single 'make' command into a multi-line exercise, SB assumes that the 'at' parameter will always be a reference of form 'end of <elements> of <object>', which is how Cocoa Scripting-based applications do it. Carbon applications don't have a single standard framework for implementing Apple event support though, so they tend to vary a bit more in their requirements. iTunes, for example, expects a reference to the container object, in this case 'source "Library"', and doesn't like it when SB passes 'end of playlists of source "Library"'. That's just how a lot of AppleScriptable applications are, but SB ignores that reality in its determination to be 'object-oriented'.
Yet more problems are caused when an application dictionary isn't 100% accurate or exhaustive in detail. Neither the aete nor sdef formats allow you to describe how an application's scripting interface works in 100% detail; some things just have to be guessed at by users, or described in supplementary documentation - the nature of Finder's 'entire contents' property being one example. Other information, such as which classes of objects can be elements of which other classes of objects, and what the type of each property is, is never actually used by AppleScript itself - it's solely there as user documentation. Since AppleScript doesn't rely on this information, any mistakes will be missed when testing the application's scripting support against AppleScript, since scripts work just fine despite it. SB does employ that information, so any typos there will result in missing or broken features that have to be circumvented by dropping down to the low-level APIs again.
Appscript, BTW, isn't 100% 'AppleScript-compliant' either, but it does come an awful lot closer. Early versions of appscript also tried to impose various OO rules on Apple events, such as enforcing the dictionary-defined object model, but after a year of running into application incompatibilities I ganked all that 'clever' code and spent the next few years trying to black-box reverse-engineer AppleScript's internal machinations and make appscript emulate them as closely as possible. "If you can't beat 'em (which you can't), join 'em", in other words. And where appscript does hit a compatibility problem, there are usually ways around it, including flipping internal compatibility settings, exporting application terminology to a module, patching it by hand, and using that instead, or dropping down to its low-level raw AE code APIs.
...
FWIW, I should also plug a few related appscript goodies.
First, the ASDictionary and ASTranslate tools on the appscript site are your friends. ASDictionary will export application dictionaries in appscript-style HTML format and also enables the built-in #help method in rb-appscript; great for interactive development in irb. ASTranslate will take an AppleScript command and (bugs willing) return the equivalent command in appscript syntax.
Second, the source distribution of rb-appscript contains both documentation and sample scripts. If you install the appscript gem, remember to grab the zip distribution for those resources as well.
Third, Matt Neuburg has written a book about rb-appscript. Go read it if you're thinking of using rb-appscript. And go read Dr Cook's paper, regardless of what you eventually decide on.
...
Anyways, hope that helps. (Oh, and apologies for length, but I've just written about 25000 words this week, so this is just some light relaxation.)
p.s. Ned, your shiny dollar is in the post. ;)