Scripting with RubyOSA

Automate your workflows with Ruby


Justin Williams Skip to comments 13 Comments (Comments Closed Closed)

Justin Williams introduces you to the RubyOSA project and shows you how to use it to automate your applications and workflows.

It's no secret that the Ruby programming language is one of the hottest languages around today. Thanks to the Ruby on Rails Web application framework, Ruby has grown from a niche language to a mainstream, viable solution that is used by programmers all over the world. That's not to say that Ruby wasn't powerful before Rails; only that it had its coming out party thanks to it. Mac OS X bundles the Ruby language with it, and a group of crafty developers have developed a solution that allows for scripters to use the Ruby language in place of AppleScript. That solution is RubyOSA: a new project from Apple.

In this tutorial I'll explain what RubyOSA is, how to install it and we'll walk through a few basic Ruby scripts that can automate our workflows.

What is RubyOSA

RubyOSA is a bridge from the Ruby language to the Apple Event Manager. The Apple Event Manger allows applications to send and receive messages, or Apple events as they are called, to and from applications that support scripting. The data from these Apple events is then available to scripters and their scripts. Apple events can be used within a single application, between multiple applications on the same computer or between computers connected via a network. All standard Carbon or Cocoa applications should be able to respond to Apple events.

RubyOSA works with standard scripting definitions (sdef) defined by an application for use via AppleScript. The sdef file is usually embedded inside of a standard Mac application and is merely an XML formatted document that defines the scripting hooks for the app. Since the sdef is made of standard XML, RubyOSA is able to easily parse it and generate a usable Ruby API. The API defines classes, methods, constants and all other necessary elements from the sdef as Ruby.

Using this Ruby-based API is no different than using AppleScript. The API does all the necessary AppleEvent work transparently for you.

RubyOSA can also work with old school Cocoa script terminology files or 'aete' resources used by Carbon and OS 9 approach.

Installing RubyOSA

Before we can begin working with RubyOSA, we need to install a few things. Since RubyOSA is still new and in beta, so it relies on some newer technologies such as RubyGems that aren't a part of the standard Mac OS X Tiger installation for Tiger. Apple has announced support for Ruby on Rails with Mac OS X Leopard, and RubyGems is a requirement of that framework, so this section will be moot come October.

To install RubyOSA, you need to be running Mac OS X 10.4.9 and Xcode 2.4 or greater. While these instructions may work with previous revisions of Tiger or Xcode, I'm building it using those versions, so why not just follow along? Xcode is not installed on the system by default, so the best place to grab it is from Apple's Developer Connection.

Note:If you already have Ruby and RubyGems installed, you can most likely skip down to the section titled Installing RubyOSA.

First, open the Terminal application. It can be found in the /Applications/Utilities folder. We'll be spending most of our time in Terminal, so hopefully you are comfortable working with it. We first need to ensure that we have our path properly set. To

We do this by checking the contents of the .bash_login file for a PATH line using a text editor such as TextMate or BBEdit. I'll be using TextMate for this tutorial, but you can adapt these instructions for your preferred editor. To open .bash_login with TextMate, type:

mate ~/.bash_login

If your path looks like the image below, you should be fine. If not, modify it to look like so:

export PATH="/usr/local/bin:/usr/local/sbin:$PATH"

Before starting the RubyOSA installation, you need to ensure your path is set correctly

Save your changes and close the file.

To make sure the changes are picked up, we need to execute the following command:

. ~/.bash_login

This will read in the .bash_login file again, which includes our newly defined PATH variable.

Next, let's setup the directory we will use to store our source code. While we could just store it on the Desktop or home directory, it's more Unix-friendly (and geeky) to keep that kind of stuff in the /usr/local directory. Type the following commands into Terminal one-by-one.

sudo mkdir -p /usr/local/src
sudo chgrp admin /usr/local/src
sudo chmod -R 775 /usr/local/src
cd /usr/local/src

Upgrading Ruby

By default, Mac OS X Tiger ships with Ruby 1.8.2. This is fine for many cases, but RubyOSA (and Rails for that matter) recommend using Ruby 1.8.5 or above. We'll go ahead and install the latest version of Ruby: 1.8.6. Before we can install that though, we should install the readline library. Readline provides a set of functions that allow users to edit command lines as they are typed in. Readline is especially useful for using the Ruby IRB shell.

Type, or cut and paste, the following commands into your Terminal window one-by-one.

curl -O ftp://ftp.gnu.org/gnu/readline/readline-5.1.tar.gz
tar xzvf readline-5.1.tar.gz
cd readline-5.1
./configure --prefix=/usr/local
make
sudo make install
cd ..

Assuming you didn't get any errors, we can move on to building Ruby itself. If you did get errors, ensure that you set your path correctly.

Ruby's compiliation and installation is just as easy as readline: more cut and pasting each line.

curl -O ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.6.tar.gz
tar xzvf ruby-1.8.6.tar.gz
cd ruby-1.8.6
./configure --prefix=/usr/local --enable-shared --enable-pthread --with-readline-dir=/usr/local 
make
sudo make install
sudo make install-doc
cd ..

After the compilation is completed, we can ensure that we're running the latest version of Ruby by calling the following command from Terminal:

ruby -v

You should see something like this:

If Ruby successfully updates, you will see version 1.8.6

RubyGems

RubyGems is a package manager that makes installing applications, libraries and frameworks that rely on Ruby incredibly easy. We will use RubyGems to install the RubyOSA library.

Type the following commands into your Terminal window.

curl -O http://files.rubyforge.mmmultiworks.com/rubygems/rubygems-0.9.2.tgz
tar xzvf rubygems-0.9.2.tgz
cd rubygems-0.9.2
sudo /usr/local/bin/ruby setup.rb
cd ..

Easy enough!

Installing RubyOSA

One of the great things about RubyGems is its ease of use. It's so easy, in fact, that we can install RubyOSA with a single command. Type the following into your Terminal window.

sudo gem install rubyosa

This will install RubyOSA and all the necessary libraries associated with it.

Getting Started With RubyOSA

With RubyOSA installed, we can test it out and ensure everything is working properly. Fire up TextMate, or your editor of choice, and type the following code into it:

#!/usr/bin/env ruby

require 'rubygems'
require 'rbosa'

app = OSA.app('iTunes')
puts app.current_track.name

This basic RubyOSA script will display the track name of the current song in iTunes

The first line the path to our Ruby installation. Next, we require the rubygems and the RubyOSA library (rbosa). RubyOSA defines the OSA namespace, which is what we will primarily be interfacing with. The first line tells our script that we want to parse the scripting dictionary from iTunes and allow it to be accessed via Ruby. We use that sdef and then output the name of the currently playing track in iTunes.

You can easily run this script by pressing Command-R.

When you run a script inside TextMate, it will print your output in a separate window

If you want to run the script outside of TextMate, save it somewhere on your machine, open a Terminal window and type

ruby /path/to/your/script.rb

You will get the same output as if you ran it from within TextMate.

Generating API Documentation

If you're not sure of all the commands that are available in an application's sdef, you can either read that via the Apple Script Editor located in /Applications/AppleScript or you can generate an Rdoc that defines the entire Ruby namespace for a specific application as an easy-to-use HTML page.

To generate the RDOC type the following into Terminal:

rdoc-osa --name iTunes

This command will create a new directory called doc at the root of where your Terminal window is, parse the sdef's XML and output it as an API document.

After generating an rdoc, you can view that documentation in Safari

A Larger Example

Our iTunes example was rather boring and basic. Let's really test out RubyOSA by building something useful. We can use RubyOSA to generate a blogroll from our NetNewsWire subscriptions. A blogroll is merely a listing of all the blogs you are subscribed to that you can list on your own personal weblog. NetNewsWire has one of the largest, and best, scripting definitions on the Mac and allows for some really interesting things to be done with it.

First, let's generate the Rdoc for NetNewsWire, so we know what types of calls we can make to the applications from Ruby.

rdoc-osa --name NetNewsWire

Take some time to look through the Rdoc and become accustomed with the scripting definition afforded to us.

Next, open up a new TextMate window and copy the following code into it.

#!/usr/bin/env ruby

require 'rubygems'
require 'rbosa'
require 'uri'

OSA.utf8_strings = true  # by default RubyOSA encodes strings as ASCII.

app = OSA.app('NetNewsWire')
app.merge('StandardAdditions')

output_file = app.choose_file_name(:default_name => "blogroll.html")

path = URI.decode(URI.parse(output_file.__data__('furl')).path)

file = File.open(path, 'w') { |f| 
f << "<ul>\n"
app.subscriptions.each do |t|
  if t.is_group?
    f << "<li><strong>#{t.display_name}</strong></li>\n"
  else
    f << "<li><a href=\"#{t.home_url}\">#{t.display_name}</a></li>\n"
  end
end
f << "</ul>\n"
}

The code for our NetNewsWire script inside TextMate

Let's walk through the code. Line's 1 - 3 should be self-explanatory. We add a third require for the URI library because we need it to parse our path later on.

Next, we set a global OSA variable that tells the bridge to encode strings using UTF8 rather than standard ASCII. UTF8 encoding is disabled by default because some applications don't support it. After we define our encoding, we tell OSA to parse the NetNewsWire sdef for use with Ruby.

Below that is an interesting line. app.merge tells our script to merge in the sdef for StandardAdditions with our NetNewsWire API. StandardAdditions is just a common library available to scripters.

Now, we are to the meat of our application. The output_file line will launch a Save file dialog that lets us determine where to save our blogroll. The line below that will parse that path into a readable string that we can pass into Ruby for writing to.

After we open our file, we output our first <ul> tag and then iterate through our subscriptions list outputting a <li> tag for each subscription we have using standard Ruby code. Notice the line that reads

if t.is_group?

This checks to see if the value output is a subscription or a folder full of subscriptions. In the case of it being a folder, we don't want to create a link and instead create bold text.

Once we have outputted all of our subscriptions, we add in a closing tag and the file is closed automatically. If you open the saved file in a browser (assuming you saved it as HTML) you will see a perfectly parsed list of blogs that you are subscribed to.

If you pop this script into the ~/Library/Application Support/NetNewsWire/Scripts folder you can access it anytime from within NetNewsWire.

For More Information

I am not a fan of AppleScript. Even though I claim to be a MacZealot, I just have never gotten into the habit of writing scripts to automate my workflows. Sure, I understand the power behind AppleScript, but its human notation always baffled me as a seasoned programmer: I just couldn't wrap my brain around it. Luckily, scripting is no longer restricted to just using AppleScript.

If you want to learn more about RubyOSA, be sure to check out the RubyOSA project Website or subscribe to the mailing list. The list is relatively low-volume, but provides announcements as well as a support group if you get stuck in your scripting.

RubyOSA is a part of Apple's continued embracing of Ruby, Python and other scripting languages as first-class citizens in the Mac OS X development world. By embracing these powerful languages as a core part of the Mac platform, they are opening up to a vast world of programmers around the world that can help grow the Mac platform even more.

Justin WilliamsJustin Williams is founder and chief author for MacZealots. He switched to the Mac almost five years ago hasn't looked back since. When not blogging or coding, you can find him watching copious amounts of TV. Justin can be reached at

Reader Comments (13)

DISCLAIMER: The views expressed below are those of their authors and not necessarily endorsed or supported by MacZealots.com. In all cases, the comments provided here are offered as a courtesy and will be moderated. Any content deemed off-topic or offensive will be removed without notice. Posting a comment here boils down to two things: 1.) Think before you type 2.) Respect the thoughts of others. See our commenting guidelines and/or privacy policy for more information.

1 Vincent Isambart remarks:
#1) On April 27, 2007 7:18 AM

Congrats for doing this very nice tutorial. However I have a few suggestions:

I think you should add —enable-shared to the configure command line to build ruby 1.8.6. If I remember well, ruby is built by default in static mode and some libraries do not like it at all (like RubyCocoa).

You should also add an header to the HTML file specifiying at least the encoding (UTF-8) because depending of your configuration your browser’s default encoding may be something else (ISO-8859-1 for example), and so the feed having names with non ASCII characters won’t be displayed correctly.

Concerning the ugly “path = URI.decode(URI.parse(output_file.__data__(‘furl’)).path)”, it should not be necessary anymore in a future version of RubyOSA. So if in the next version this code does not work anymore, you will probably just have to remove this line and replace “path” in the folowing line with “output_file”.

2 Justin Williams remarks:
#2) On April 27, 2007 10:03 AM

Thanks for the suggestions, Vincent. I’ll update the article accordingly.

3 Scott Stevenson remarks:
#3) On April 28, 2007 2:59 AM

Excellent work.

4 Kenny remarks:
#4) On May 3, 2007 1:37 AM

Is there any way to have ruby-osa work in places like folder actions?

5 Adrian M remarks:
#5) On May 3, 2007 2:43 AM

When I saw this article, I thought it would be a proper installed OSA language variant (listed by osalang), but it’s not. If it did, then it could be used for folder actions and wotnot.

Still good though.

FWIW, I got this running by using MacPorts…

sudo -s
port install ruby
port install rb-rubygems

6 has remarks:
#6) On May 3, 2007 8:20 AM

Adrian: you may have been thinking of Philip Aker’s RubyOSA component (no relation):

http://homepage.mac.com/philip_aker/osa/osa.html

The latest release supports Mail rule actions, although I don’t know about folder actions - it’s a fairly basic component supporting only a subset of the standard OSA API plus some hardwired extras like Mail rule support, so you’d need to investigate.

7 Jim Schimpf remarks:
#7) On May 4, 2007 12:45 PM

Thanks for the very nice tutorial, I worked on it over lunch. Now to the problem, I have the new ruby installed and gem installed but when I try to get rubyosa I get:

[ST-PowerBook:~] jim% sudo gem install refresh
ERROR: While executing gem … (NoMethodError)
undefined method `refresh’ for #

Any suggestions ? Thanks.

—jim

8 Tait remarks:
#8) On May 6, 2007 3:23 PM

Addendum to the instructions:

Just after compiling and installing the new version of Ruby, running ‘ruby -v’ reported the older version (1.8.2). I thought I had done something wrong, perhaps. However, re-running ‘ruby -v’ after restarting iTerm did report the correct version.

9 Charlie remarks:
#9) On May 8, 2007 11:45 AM

Everything went fine until I tried the first RubyOSA program. the line “puts app.current_track.name” generates the error:
/usr/local/lib/ruby/gems/1.8/gems/rubyosa-0.4.0/lib/rbosa.rb:558:in `__send_event__’: application returned error: Runtime resolution of an object failed. (-1728) (RuntimeError)
from /usr/local/lib/ruby/gems/1.8/gems/rubyosa-0.4.0/lib/rbosa.rb:558:in `name’
from itunes.rb:7
Doing it one step at a time reveals that everything works and seems to return valid objects except the final step of reading the value of “name”. Trying other attributes (like “description”) also fails. And accessing attributes of other objects also fails. Any ideas on what I am doing wrong?

10 Kim remarks:
#10) On May 9, 2007 4:52 AM

I think i had to set up my entire gems repository anew (delete the old one & download your gems again with /usr/local/bin/ruby gem install ,..) after updating to Ruby 1.8.5! Then everything worked fine again!!!

11 Chinmoy remarks:
#11) On May 21, 2007 1:58 PM

Thanks for the tutorial. There is also the rb-appscript binding, which is similar to RubyOSA (I am not affiliated with rb-appscript).

12 Peter remarks:
#12) On August 26, 2007 4:32 AM

This looks great. However, does anyone else get this error when trying to generate API documentation:

:/usr/local/src/readline-5.1 petermarks$ rdoc-osa —name iTunes

Directory doc already exists, but it looks like it
isn’t an RDoc directory. Because RDoc doesn’t want to risk
destroying any of your existing files, you’ll need to
specify a different output directory name (using the
—op option).

Error when executing `rdoc —template ‘/usr/local/lib/ruby/gems/1.8/gems/rubyosa-0.4.0/data/rubyosa/rdoc_html.rb’ —title ‘iTunes RubyOSA API’ —main OSA “/tmp/iTunes-0-7584.rb”’ : 256

13 Michael Ypes remarks:
#13) On April 26, 2009 2:30 PM

Hey Justin, thanks for the article. Finally got it working although the file does not save itself . You have to save it via the application itself. Am I doing something wrong… Also, have you got any examples of Photoshop CS3 which is actually what I am trying to work on. I have found one for CS2 and it doesn’t work?

By the way, I think you got a spam issue on this article :)

Cheers

M