Mac OS X provides rich scripting and automation tools that can simplify everyday tasks—if you know how to use them. The roster includes the venerable command line, the graphical Automator utility, and the traditional AppleScript natural-language scripting environment. Although these tools all have value in certain situations, they each have some real limitations.

For instance, AppleScript benefits from extremely tight platform integration and powerful support for manipulating user interface elements, but its eccentric syntax and limited functionality constrain the scope of its applicability. AppleScript simply isn't designed to serve as a general-purpose scripting language.

Fortunately, users have another solution to call upon in situations where they want both platform integration and the flexibility of a mainstream programming language. The availability of MacRuby bindings for Apple's Scripting Bridge allows users to take advantage of script extensibility interfaces exposed by Cocoa applications, and to do so from the comfort of a clean and modern general-purpose scripting language.

MacRuby, an open source project partly supported by Apple, is a special Ruby implementation that interoperates with Apple's development stack. It runs on top of the Objective-C runtime and largely reconciles Ruby's standard types with the equivalent Cocoa foundation classes—so deeply, in fact, that all objects in the MacRuby execution environment are descendants of NSObject.

MacRuby is an extraordinarily powerful tool when used to its full potential. We are going to be looking solely at automation and application scripting usage scenarios in this tutorial, but it's worth noting that MacRuby supports much more—including the development of full-blown Cocoa desktop applications. Given sufficient interest, we could explore MacRuby more broadly in future coverage.

To run the example scripts in this tutorial, you will need to install MacRuby on your Mac. You can download it from the project's website.

A first step

Let's start with a trivial example, a script that displays the title of the active tab in each open Safari window. In order to do that, we will need to look at each Safari window, obtain the current active tab, and extract the name. Here's how to do it in MacRuby:

#!/usr/local/bin/macruby framework "ScriptingBridge" safari = SBApplication.applicationWithBundleIdentifier("com.apple.Safari") safari.windows.each {|window| puts window.currentTab.name}

Like any script intended to be run in a UNIX command line environment, the first line includes a shebang and the path to the desired interpreter. The framework keyword used on the second line will load the specified Cocoa framework; we obviously need to load the Scripting Bridge in order to use its functionality.

The third line instantiates an object that exposes Safari's scripting interface. SBApplication is a Cocoa class from the Scripting Bridge framework—you can easily find it in the Cocoa reference documentation. The applicationWithBundleIdentifier method allows us to indicate which application we want to connect to by supplying a bundle identifier as the parameter.

In the final line, we iterate through Safari's windows and echo the name of the active tab on stdout for each window instance. As you can see, MacRuby graciously allows us to use Ruby's Array::each method and standard block syntax for the iteration. We also get to use the standard property access syntax.

Before we continue, I want to take a quick detour to talk about the bundle identifier. Each application bundle on Mac OS X has its own bundle identifier code, which usually takes the form of three strings separated by periods. Although you could use a filesystem path instead, it's generally a good idea to use the bundle identifier to specify which application you want to control.

The bundle identifier is safer than filesystem paths for a portable script, because it is guaranteed to be consistent between systems. To figure out the bundle identifier for an application, you can use the following command line command to peek at the relevant Spotlight metadata:

mdls -name kMDItemCFBundleIdentifier /Applications/Safari.app

Understanding the APIs

If you don't have much previous experience with Mac scripting, you might be wondering how you are supposed to figure out the properties and methods that are available through the Scripting Bridge for each application. The easiest way is to use the AppleScript dictionary viewer.

Applications that support the Scripting Bridge define their scripting APIs in an XML-based data format. You can find an application's scripting API definition by descending into an app bundle and looking for a file with the .sdef extension. These files are human-readable, but it's more convenient to view them with a graphical interface.

Start by launching the AppleScript Editor, a standard utility that ships with Mac OS X. After the editor is running, select the Open Dictionary option from the File menu. The program will display a list of all the app bundles it can find that have an associated .sdef file. When you double-click an application in that list, the AppleScript Editor will display a browsable view of the scripting APIs available from the target application.

The one downside is that the tool will display the property and method names in formats intended for AppleScript programmers; there will be cases where the actual names are different in MacRuby due to syntactic considerations. AppleScript allows spaces in property names, for example, which have to be removed in Ruby.

Fortunately, standard Ruby introspection methods will mostly work as expected in MacRuby, even with the Scripting Bridge. In the Safari example above, you could call puts safari.methods to see what functionality is accessible.

Peering into Evernote

Apple's applications aren't the only ones accessible through the Scripting Bridge. Many third-party developers also integrate scripting support into their Mac software. One app that has a particularly nice set of scripting APIs is Evernote, the popular cloud-centric note-taking program.

This means that users who want to integrate with Evernote on the Mac desktop can connect directly to the application and avoid having to deal with authentication or any of the other complexities that arise from working with the Evernote Web service APIs.

This example will demonstrate how to traverse all of the user's notebooks and notes in Evernote and echo a bulleted list of the names to stdout . Keep in mind that it's accessing the application on the local computer, so it will only be aware of notes that have been synced. Another important thing to remember is that the application has to be running in order to be accessible to the Scripting Bridge. When you call applicationWithBundleIdentifier , it will automatically launch the application if it isn't already running.

#!/usr/local/bin/macruby framework "ScriptingBridge" evernote = SBApplication.applicationWithBundleIdentifier("com.evernote.Evernote") evernote.notebooks.each {|notebook| puts "* #{notebook.name}" notebook.notes.each {|note| puts " - #{note.title}"} }

The behavior of the example above should be immediately obvious to anyone with prior Ruby programming experience. We iterate over the notebooks array, display the value of the name property, and then iterate over the notes array for the notebook so that we can display the title property. I got the property names right out of the scripting dictionary for Evernote in exactly the manner that I explained in the previous section.

When you work with the scripting bridge, the arrays and most of the other values that it passes back to Ruby are immutable. But MacRuby provides setter functions that wrap the modifiable properties, allowing you to change their values. Even though all the data structures look and quack like native Ruby data structures, it's important to remember that they aren't.

To illustrate that point, consider the following code, which is a somewhat contrived example that shows how you can reverse the title of every note:

#!/usr/local/bin/macruby framework "ScriptingBridge" evernote = SBApplication.applicationWithBundleIdentifier("com.evernote.Evernote") evernote.notebooks.each {|notebook| notebook.notes.each {|note| note.title = note.title.reverse } }