macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Replacing AppleScript with Ruby
Pages: 1, 2

Integrating Apple Events into Ruby

The true benefits of using Ruby instead of AppleScript as a base for sending Apple events to scriptable applications emerge when you consider the relative merits of the two languages. You shed the clumsy, tricky verbosity of AppleScript, along with its gaping linguistic holes (such as the paucity of string-handling abilities) in favor of such Ruby elegances as regular expressions, iterators, blocks, and true object-orientation—not to mention all the power of Ruby's vast built-in classes, libraries, and gems.



To illustrate this point, we'll rewrite the example from page 415 of my book to use Appscript. The rewrite will be fairly substantial, because my grasp of Ruby idiom at the time of its development was even more rudimentary than it is now. The script does something quite useful. First, it reads a text file, specified as an argument on the command line, and tallies the number of occurrences of each individual word (that is, it constructs a histogram of word usage). This is a typically simple Ruby task, thanks to hashes and regular expressions. Then it calls upon Microsoft Excel to draw a bar chart of the relative frequency of the 30 most common words in the file.

Here's the start of the script:

#!/usr/bin/env ruby
require 'appscript'
include Appscript

module Enumerable
  def each_with_index1 # AppleScript indices usually 1-based
    self.each_with_index {|item, index| yield item, index + 1}
  end
end

We begin by inserting a convenience method into the built-in Enumerable module, allowing us to mediate easily during iterations between Ruby index numbers, which are 0-based, and AppleScript index numbers, which are 1-based.

Next, we open the specified file, construct the histogram, and close the file:

h = Hash.new(0)
open(ARGV[0]).each do |line|
  line.scan(/\w+[-']*\w+/) do |w| # allow internal punctuation
    h[w.downcase] += 1
  end
end
h = h.sort{ |x,y| (x[1] <=> y[1]).nonzero? || y[0] <=> x[0] }

Words are hashed without respect to case. The regular expression used to define the notion of a word conceals some arbitrary but reasonable decisions: we ignore words consisting of only one letter, and we permit a word to contain a hyphen or an apostrophe.

The final line transforms the hash into an array of key-value pairs (that is, each word paired with the number of times it occurs in the document), sorted in the following order: words that occur more times in the file appear later in the array, and among words that occur the same number of times, a word that comes earlier in alphabetical order appears later in the array. The last 30 entries in the array are thus, in reverse order, the 30 most frequent words in the file. The order is intentionally reversed because this is the easiest way to construct the chart the way I want it.

Now we start talking to Excel. First, we bring Excel to the front and create a new workbook, capturing a reference to its first worksheet as a variable (w1):

excel = app('Microsoft Excel')
excel.activate
excel.make(:new => :workbook)
w1 = excel.worksheets[1]

Next, we enter the last 30 entries in the array as data in the worksheet. Words are placed in the Excel range A1:A30 (the most frequently used word thus appearing in A30), with their frequencies beside them in the range B1:B30. This is the iteration for which we created Enumerable#each_with_index1 earlier:

h[-30..-1].each_with_index1 do |pair, row|
  pair.each_with_index1 { |val, col| w1.rows[row].cells[col].value.set(val)}
end

The following screenshot shows the data as entered into Excel when the script was run against a partially completed first draft of this article:

data entered into Excel
Figure 1. The data as entered into Excel

(The article was written in HTML, and the code does nothing to eliminate tags, so pseudowords such as "pre" and "code" are included in the list.)

At last we are ready to construct the chart.

excel.ranges["A1:B30"].select
excel.make(:new => :chart_sheet, :at => excel.active_workbook.start)

The remainder of the script is simply a matter of formatting the chart the way I want it—namely, a bar chart with the title "30 Most Frequent Words," with no legend, with data labels on the bars (so that each bar ends with the word it represents), and with no gridlines on any axis. Excel's AppleScript commands and object model are effectively a wrapper around its Visual Basic commands and object model, and are peculiarly unidiomatic. So, the resulting code is rather odd-looking, but this is due to Excel, not to Appscript.

c = excel.active_chart
c.chart_type.set(:bar_clustered)
c.has_title.set(true)
c.chart_title.caption.set("30 Most Frequent Words")
c.has_legend.set(false)
c.apply_data_labels(:type_ => :data_labels_show_label, :legend_key => false)
[:category_axis, :series_axis, :value_axis].each do |axt|
  [:primary_axis, :secondary_axis].each do |ax|
    begin
      x = c.get_axis(:axis_type => axt, :which_axis => ax)
      x.has_major_gridlines.set(false)
      x.has_minor_gridlines.set(false)
      x.has_title.set(false)
      c.set_has_axis(:axis_type => axt, :axis_group => ax, :axis_exists => false)
    rescue
    end
  end
end

The two nested each do loops are a way of iterating through every possible axis of the chart (because Excel doesn't let us ask for all of them at once). In reality, of the resulting six axes, only two are capable of having their has major gridlines property set, but I am too impatient to worry about which ones these are. So I wrap the interior of the loop in a begin...rescue...end block, which is the Ruby equivalent of an AppleScript try block. If an axis doesn't have gridlines, a runtime error occurs; the rescue block is empty of code and blithely ignores the error, and we proceed to the next iteration.

Here's the resulting chart, corresponding to the data from the earlier screenshot:

chart
Figure 2. The resulting chart

Conclusions

Finding an alternative way of sending Apple events is not merely a matter of petulant impatience with the AppleScript language, it may very well be the path to the future. The Apple event/AppleScript mode of scripting applications dates back well over a decade and a half to Macintosh System 7. Apple events themselves are unlikely to go away any time soon, and in many ways they don't deserve to. An Apple event is a remarkably sophisticated way of requesting and communicating information, and Cocoa Scripting is improving with each major Mac OS system release, making it easier and easier for Cocoa programmers to wrap scriptability around their applications. AppleScript, on the other hand, is implemented by what is probably the oldest code remaining in any corner of the system. That code—which is probably very difficult to revise substantially at this point—was originally intended to run in a tiny RAM space, and the language that it implements made a number of assumptions about what would make scripting "easier" for the naive user, who was presumed to be afraid of the notion of doing any programming.

But the user of today is not like the imagined user of the "appliance" Macintosh of 1991. The developer tools are free. "Ordinary" users are churning out Dashboard widgets written in JavaScript. The Unix heritage of Mac OS X has brought into the user base large numbers of users familiar with Perl and shell scripting; modern Web applications run on PHP; and of course the popularity of Ruby itself is increasing at what seems an exponential rate. In the near future, the Microsoft Office applications are slated to lose Visual Basic support on the Mac, and VBA experts will be seeking an alternative language in which to recast their existing macros. On Windows, furthermore, there is already a bridge between Ruby and scripting the Office applications, so Windows refugees would naturally look for an equivalent on Mac OS. All these users are mature and experienced enough to recognize that scripting is programming and that a programming language is acquired through study. When confronted with AppleScript as the dominant language for scripting Mac applications, they may naturally feel a certain disappointment, not to mention disbelief.

In this environment, and in the spirit of "the more, the merrier," adding to the scripter's toolbox the ability to send Apple events easily from Ruby can only be a good thing. Appscript is a remarkably well-implemented library, by a developer with a deep, highly technical, and altogether practical understanding of Apple events and AppleScript. It has reached a sufficiently mature stage of development to be reliably usable; what it needs now is more users and more feedback. I hope this article has encouraged you to give it a try.

Matt Neuburg is the author of O'Reilly's "AppleScript: The Definitive Guide," "REALbasic: The Definitive Guide," and "Frontier: The Definitive Guide," and is a former editor of MacTech magazine.


Return to the Mac DevCenter.