AddThis Social Bookmark Button

Print

Replacing AppleScript with Ruby

by Matt Neuburg
02/27/2007

When a Mac application is said to be scriptable, this usually means that it is scriptable through AppleScript. The application supplies a dictionary that translates between (on the one hand) some English-like vocabulary, employed by the end user while targeting the application with the AppleScript language, and (on the other hand) the Apple events that constitute the medium of interapplication communication on Mac OS.

My book, AppleScript: The Definitive Guide, describes and teaches the AppleScript language, but it also bemoans that language's many failings. I won't enumerate those failings here; let's just stipulate that for some users, typically those with some experience of other programming languages, scripting Mac applications via Apple events would be a much more pleasant experience if AppleScript weren't involved. In fact, I happen to be one such user.

For this reason, the second edition of my book includes an Appendix entitled "Apple Events Without AppleScript," which describes several alternative ways to construct and send Apple events to scriptable applications. One such way is Appscript, by Hamish Sanderson, which idiomatically injects AppleScript's interapplication communications capabilities into Python. Since the book was written, the world has not stood still; in particular, Sanderson has been developing a Ruby version of Appscript, dubbed rb-appscript.

It would be a waste of space for me to describe the Ruby language or to argue its virtues. Let's just stipulate, once again, that many people find Ruby's linguistic and conceptual world a powerful and satisfying place for programming. Again, I am such a user. At the O'Reilly Mac OS X Conference, way back in October of 2003, I presented a talk that included an example of how to access AppleScript from Ruby, an example that was later incorporated into my book (page 415 in the second edition). But accessing AppleScript and bypassing it entirely are very different things; thanks to rb-appscript, the latter is now possible.

In the rest of this article, I'll explain how to download and install rb-appscript. I'll discuss the basics of rb-appscript usage and show how to develop a simple "hello world" script. Finally, I'll rewrite the Ruby-AppleScript example from my book to use Ruby with rb-appscript instead.

Downloading and Installing Appscript for Ruby

Presumably you're already up and running with Ruby. Although rb-appscript is available as a Ruby gem, I prefer manual installation from source. Obtain the latest build from RubyForge. The file you want to download is the source .zip file. (At the time of writing, it was called "rb-appscript-0.3.0.zip.") Unzip the archive, cd into the unzipped directory in the Terminal, and type:

$ ruby extconf.rb
$ make
$ sudo make install

To test, use your favorite Unix script-running environment, and create and execute this script:

#!/usr/bin/env ruby
require "appscript"
include Appscript
puts app('Finder').name.get

The result should be Finder. Congratulations: you're sending and receiving Apple events with Ruby. Before you throw away the unzipped archive, retain the doc folder that it contains. It holds the Appscript documentation as a set of interlinked HTML pages. To get started studying the documentation, open the file index.html in your browser.

Appscript Syntax

The expression app('Finder').name.get exemplifies how Appscript mediates between AppleScript's English-like terminology and Ruby. The application to be addressed is an Application object, typically generated by the app() convenience method. That application's AppleScript object model is navigated by means of method calls whose Ruby names are based on the AppleScript English-like term for a property or element. Element specifiers are expressed using plurals. Specifiers by index and name use array and hash element syntax, respectively:

app('Finder').folders[1].name.get
app('Finder').files["hello"].modification_date.get

Specifiers by position, range, and Boolean test are rather more elaborate. For example:

app('Finder').items[its.name.does_not_contain("e")].name.get

AppleScript commands are applied as method calls as well. The method can take parameters in the usual way; if the AppleScript command has named parameters, a hash is used:

app('Finder').folders[1].count(:each => :item)

Commands that would return a reference in AppleScript return a Reference object in Appscript; its properties and elements are expressed in just the same way as for an Application object. Appscript, unlike AppleScript, does not attempt to be annoyingly "clever" by implicitly dereferencing references for you. Thus, there is no need for an Appscript equivalent to the AppleScript a reference to operator, since a reference is not dereferenced unless you explicitly apply a method requesting its value, such as get():

some_file = app('Finder').files[1] # a reference
some_file_name_ref = some_file.name # still a reference
puts some_file_name_ref.get # => "aaa", a string

In certain situations, you need (for syntactical purposes) to form a reference without asking the target application to fetch that reference (that is, without sending an extra Apple event). To do so, construct the reference using methods on the bare app object (as opposed to an application specifier). These two expressions are equivalent in Appscript:

app('Finder').folders[1].count(:each => :item)
app('Finder').count(app.folders[1], :each => :item)

That example illustrates also that commands, such as get() and count(), are actually methods of the application object, as shown in the second version. For convenience, though, Appscript permits a command to be applied as a method call to what AppleScript calls the command's "direct parameter," in which case the direct parameter may be omitted from the command's parameters, as shown in the first version.

Coercion (AppleScript as) is performed by a named parameter called result_type:

app('Finder').folders[1].get(:result_type => :alias)

The following example shows how Appscript lets you call Scripting Addition commands:

#!/usr/bin/env ruby
require "osax"
include OSAX
p osax.info_for(osax.path_to(:desktop))
# => {:name=>"Desktop", :kind=>"Folder", :visible=>true, ... }

Where possible, values are translated transparently between equivalent AppleScript and Ruby types; so, as we have seen, an AppleScript record is equivalent to a Ruby hash. Moreover, Appscript is remarkably complete in the degree to which Apple events and their subtleties can be expressed. There are Appscript equivalents of the AppleScript timeout block, of the rarely used transaction feature, and more. You can express obscure built-in AppleScript unit classes such as inches. And if you really have to, you can even construct a raw Apple event from scratch (though I won't discuss how to do that here).

The upshot is that there is hardly any learning curve for the AppleScript user wishing to switch to Ruby. Syntactical rules for how Appscript expresses element specifiers and command parameters are quickly assimilated from examples provided in the documentation. The rest of the art of using Appscript is a matter of mere vocabulary—that is, of knowing how an AppleScript term is "translated" into the name of a Ruby method. This is not at all troublesome, since the translation is performed by simple rules, enunciated in the documentation (look for the document entitled "Keyword Conversion").

For instance, we've seen that the Ruby method names are all lowercase, with an underscore where AppleScript would have a space. AppleScript class names, enumerator names, and parameter names are expressed as Ruby symbols. Appscript also provides several methods on an Application or Reference object that allow you to list its properties, elements, commands (and their parameter names), and other AppleScript terms, as translated into Ruby.

Hello, World

In Appendix B of my book, the example used is this script:

tell application "BBEdit"
    make new document
    tell document 1
        set its text to "Hello, world!"
    end tell
end tell

The Appendix shows how to generate the same Apple events as this script does using a variety of alternatives to AppleScript, such as Cocoa/Carbon raw Apple event construction, UserLand Frontier, Perl, and Python. To complete the Appendix, here it is in Ruby:

#!/usr/bin/env ruby
require "appscript"
include Appscript
bb = app("BBEdit")
bb.make(:new => :document)
bb.documents[1].text.set("Hello, world!")

Pages: 1, 2

Next Pagearrow