Tutorial: OS X automation with MacRuby and the Scripting Bridge


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:

  1. #!/usr/local/bin/macruby
  2. framework “ScriptingBridge”
  3. safari = SBApplication.applicationWithBundleIdentifier(“com.apple.Safari”)
  4. safari.windows.each {|window| puts window.currentTab.name}
#!/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.

  1. #!/usr/local/bin/macruby
  2. framework “ScriptingBridge”
  3. evernote = SBApplication.applicationWithBundleIdentifier(“com.evernote.Evernote”)
  4. evernote.notebooks.each {|notebook|
  5. puts “* #{notebook.name}”
  6. notebook.notes.each {|note| puts ” – #{note.title}”}
  7. }
#!/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:

  1. #!/usr/local/bin/macruby
  2. framework “ScriptingBridge”
  3. evernote = SBApplication.applicationWithBundleIdentifier(“com.evernote.Evernote”)
  4. evernote.notebooks.each {|notebook|
  5. notebook.notes.each {|note| note.title = note.title.reverse }
  6. }
#!/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 }
}

In truly idiomatic Ruby code, we would typically do something like note.title.reverse! to reverse the value in place rather than using the more verbose formulation of reversing the value and assigning it back to the property. If you were to do that with the Scripting Bridge, however, you would get an error message telling you that name.title is an immutable string. The reason the assignment form works is because it’s calling a title= method (basically an alias for setTitle) that properly sends the updated value through the Scripting Bridge

Creating a new note

In our next example, we’re going to build something a bit more useful. When I’m working at the command line, I often want to save the output of a command for later reference. To serve that purpose, I made a little Ruby script that will allow me to pipe content into a new Evernote note. As some of you might remember, I made made a similar tool for Linux users back in 2007 with Tomboy’s D-Bus bindings. The following is the Ruby and Evernote equivalent:

  1. #!/usr/local/bin/macruby
  2. framework “Foundation”
  3. framework “ScriptingBridge”
  4. require “Time”
  5. title = ARGV[0] || “Command output on #{Time.now}”
  6. evernote = SBApplication.applicationWithBundleIdentifier(“com.evernote.Evernote”)
  7. evernote.createNoteFromFile(
  8. nil, fromUrl:nil, withText:STDIN.reaad, withHtml:nil,
  9. title:title, notebook:evernote.notebooks[0],
  10. tags:[“commandline”], attachments:nil, created:nil)
#!/usr/local/bin/macruby

framework "Foundation"
framework "ScriptingBridge"

require "Time"

title = ARGV[0] || "Command output on #{Time.now}"

evernote = SBApplication.applicationWithBundleIdentifier("com.evernote.Evernote")

evernote.createNoteFromFile(
    nil, fromUrl:nil, withText:STDIN.reaad, withHtml:nil,
    title:title, notebook:evernote.notebooks[0],
    tags:["commandline"], attachments:nil, created:nil)

The script reads the contents of stdin and puts this into a new note. The user can optionally provide the title of the note as a parameter at the command line. If no parameter is provided, the script will generate a default title with the current time. It puts the note into the user’s main notebook and adds a “commandline” tag.

Two things are striking about this example. You have probably noticed that we use the Time module from Ruby’s standard library. You can use any standard library modules or external gems in MacRuby just like you would in regular Ruby, and that’s a huge part of what makes MacRuby valuable for system automation. If you want to do JSON parsing, or work with a REST API, or any number of other similar tasks, you can use either Ruby modules or Cocoa frameworks. You aren’t limited to the functionality exposed by Apple’s scripting system.

The second thing that you might have noticed about the example above is that the function invocation used to create the note isn’t exactly standard Ruby. The colons are a convention from Objective-C selector syntax that is supported by the MacRuby parser for convenience.

When you are invoking functions with multiple parameters across the Scripting Bridge, you will often have to use that form. For the purposes of the Scripting Bridge, you have to provide a value for every key and you have to make sure that you put them in the right order. If you don’t actually need a key, simply give it the nil value.

In the example above, the function provides different keys for populating the note in different ways. We want to use plain text for our note, so we use the withText: key. If you wanted to give the note content proper fixed-width font formatting, you might instead use withHtml: and wrap the text in a pre tag.

One more for the road

The Scripting Bridge allows you to connect to as many programs as you want in your Ruby scripts. For our last example, I’ll show you how to use it with two programs at once. We are going to iterate through an iTunes playlist, find all of the tracks with a rating higher than 3 stars, sort them by the number of times that they have been played, and then drop them into an Evernote note as a numbered list.

  1. #!/usr/local/bin/macruby
  2. framework “ScriptingBridge”
  3. require “Time”
  4. iTunes = SBApplication.applicationWithBundleIdentifier(“com.apple.iTunes”)
  5. favsongs = iTunes.sources
  6. .find {|s| s.name == “Library”}
  7. .playlists.find {|p| p.name == “Top 25 Most Played”}
  8. .tracks.find_all {|t| t.rating > 3}
  9. .sort {|t1,t2| t2.playedCount <=> t1.playedCount}
  10. .map {|t| “<li>#{t.artist} – #{t.name} (#{t.playedCount})<li>”}
  11. evernote = SBApplication.applicationWithBundleIdentifier(“com.evernote.Evernote”)
  12. evernote.createNoteFromFile(
  13. nil, fromUrl:nil, withText:nil,
  14. withHtml:”<ol>#{favsongs.join(“\n”)}</ol>”,
  15. title:”Favorite songs on #{Time.now.strftime(“%Y-%m-%d”)}”,
  16. notebook:evernote.notebooks[0],
  17. tags:nil, attachments:nil, created:nil)
#!/usr/local/bin/macruby

framework "ScriptingBridge"

require "Time"

iTunes = SBApplication.applicationWithBundleIdentifier("com.apple.iTunes")
favsongs = iTunes.sources
  .find {|s| s.name == "Library"}
  .playlists.find {|p| p.name == "Top 25 Most Played"}
  .tracks.find_all {|t| t.rating > 3}
  .sort {|t1,t2| t2.playedCount <=> t1.playedCount}
  .map {|t| "<li>#{t.artist} - #{t.name} (#{t.playedCount})<li>"}

evernote = SBApplication.applicationWithBundleIdentifier("com.evernote.Evernote")

evernote.createNoteFromFile(
    nil, fromUrl:nil, withText:nil,
    withHtml:"<ol>#{favsongs.join("\n")}</ol>",
    title:"Favorite songs on #{Time.now.strftime("%Y-%m-%d")}",
    notebook:evernote.notebooks[0],
    tags:nil, attachments:nil, created:nil)

As you can see from the example, we use Ruby’s standard find, find_all, sort, and map array methods. Chaining together Ruby blocks in this manner can be a really convenient and syntactically expressive way to dig into the contents of Scripting Bridge data structures. We also use the Time module so that the note will have a unique title. If you wanted to go even further with this example, you could use some Ruby code to grab album art from Amazon or details about bands from Wikipedia and add it to the HTML that you put in the note.

The examples in this tutorial have all been relatively simple—you could do some of the more basic ones just as easily in AppleScript. If you want to scale up and put this functionality to work in more sophisticated use cases, however, Ruby becomes a valuable ally.

You could, for example, use Evernote as a blogging tool and make a script that automatically uploads your notes to a WordPress blog via an XML-RPC API. Or you could iterate through your iPhoto collection, export images that match certain parameters, transform them with RMagic, and upload them to a service like Imgur.

MacRuby opens the door for all kinds of useful integration between local system applications and Web services. If you get really ambitious, you can also use MacRuby to build custom graphical interfaces for your automation tools by importing the AppKit framework and using Cocoa widgets. It’s even possible to bundle up MacRuby scripts with Cocoa interfaces as stand-alone app bundles so you can deploy them to other users like regular applications.

If you want a comprehensive introduction to using MacRuby for Cocoa development, I highly recommend Matt Aimonetti’s book, MacRuby: The Definitive Guide. You can also refer to the MacRuby website for more technical details.

source : arstechnica.com

Tinggalkan Balasan

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s