Introduction to Ruby for Mac OS X

The Principle of Lease Surprise

This article was originally published as the cover article in
/MacTech Magazine/ 19.3 (March 2003). It has been reformatted
for this site.

Introducing Ruby

Yukihiro “Matz” Matsumoto was looking for an object oriented scripting language. Python wasn’t OO enough. Perl’s “Swiss Army Chainsaw” approach was too messy. When he didn’t find what he was looking for, Matz decided to write his own language. His goal was to create one good enough to replace Perl. Ruby was born in February of 1993 and first released to the public in 1995. Today, Ruby is more popular than Python in Japan. Ruby started hitting the shores of the United States around 2000.

Here’s how I think of Ruby. Take Smalltalk, where everything—even a number—is an object. Make it a file-based, interpreted scripting language. Give it a syntax familiar to Perl and Python users. Add the best features of many different languages such as regular expressions, iterators, block closures, garbage collection, and dynamic typing. Abstract many features into classes like Regexp and mixin modules like Enumerable. Deliver it with a mature, useful set of library modules. Finally, add a helpful and responsive developer community. The result is a language that is easy to learn, powerful, and a joy to use.

I’ve fallen in love with Ruby. It’s a pure object oriented scripting language. It’s simple, consistent, and powerful. It stays out of my way and makes me more productive. Best of all, it comes pre-installed with Jaguar. Open a terminal window and type ruby -v. See? If you aren’t yet running Jaguar, you can download Ruby from the Ruby home page and compile and install it yourself.

To attempt to illustrate why I like Ruby so much, let’s take a look at the same class written in Java, Objective-C, Perl, and Ruby. Listing 1 defines a Song class with two instance variables, accessor (getter and setter) methods, a method that converts the song to a string for printing, and code to create a Song object and print it. The Ruby code is smallest and cleanest (therefore easiest to understand and maintain) without being terse or obfuscated.


Listing 1: song.java, song.m, song.pl, song.rb


Java, Objective-C, Perl, and Ruby code samples that each do
the same thing: define a simple Song class and use it.

// ======== Java (song.java) ========

public class Song {

    protected String name;
    protected int lengthInSeconds;

    Song(String name, int len) {
        this.name = name;
        lengthInSeconds = len;
    }

    public String getName() { return name; }
    public void setName(String str) { name = str; }

    public int getLengthInSeconds() {
        return lengthInSeconds;
    }

    public void setLengthInSeconds(int secs) {
        lengthInSeconds = secs;
    }

    public String toString() {
        return name + " (" + lengthInSeconds + " seconds)";
    }

    // Create and print
    public void main(String[] args) {
        s = new Song("name", 60);
        System.out.println(s);
    }
}

// ======== Objective-C (song.m) ========

#import <Foundation/NSString.h>

@interface Song : NSObject
{
    NSString *name;
    int lengthInSeconds;
}

- initWithName:(NSString *)name length:(int)length;
- (void)dealloc;

- (NSString *)name;
- (void)setName:(NSString *)name;

- (int)lengthInSeconds;
- (void)setLengthInSeconds:(int)length;

- (NSString *)description;
@end

@implementation Song

- initWithName:(NSString *)nameString length:(int)length
{
    [super init];
    [self setName:nameString];
    [self setLengthInSeconds:length];
    return self;
}

- (void)dealloc
{
    [name release];
    [super dealloc];
}

- (NSString *)name { return name; }

- (void)setName:(NSString *)nameString
{
    [name autorelease];
    name = nameString;
    [name retain];
}

- (int)lengthInSeconds { return lengthInSeconds; }

- (void)setLengthInSeconds:(int)length
{
    lengthInSeconds = length;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"%@ (%d seconds)",
        [self name], [self lengthInSeconds]];
}

@end

int main(int argc, char *argv[])
{
    // Create and print
    Song *song = [[Song alloc] initWithName:@"name"
                     length:60];
    NSLog(@"%@", song);
    return 0;
}

// ======== Perl (song.pl) ========

package Song;

sub new {
    my($class, $name, $len) = @_;
    my $self = {};
    $self->{'name'} = $name;
    $self->{'lengthInSeconds'} = $len;
    bless $self, class;
    return $self;
}

sub name {
    my($self) = shift;
    if (@_) { $self->{'name'} = shift }
    return $self->{'name'};
}

sub lengthInSeconds {
    my($self) = shift;
    if (@_) { $self->{'lengthInSeconds'} = shift }
    return $self->{'lengthInSeconds'};
}

sub toString {
    my($self) = shift;
    return $self->name() . "(" . $self->lengthInSeconds()
        . ") seconds";
}

# Create and print
$s = Song->new('name', 60);
print $s->toString() . "\n";

// ======== Ruby (song.rb) ========
class Song

  # Not only declare instance variables (which is
  # unnecessary) but also create accessor methods
  # (getters and setters).
  attr_accessor :name, :length_in_seconds

  # The constructor, sort of. This method is called
  # by the class method "new".
  def initialize(name, len)
    @name = name
    @length_in_seconds = len
  end

  def to_s
    return "#{@name} (#{@length_in_seconds} seconds)"
  end
end

# Create and print.
s = Song.new('name', 60)
puts s

This article will cover Ruby’s language features, syntax, built-in classes, and libraries. We’ll review a few Mac OS X-specific modules, look at some sample code, and leave you with a list of resources. This isn’t a tutorial or a complete description of Ruby’s features and idioms. There are a number of excellent Ruby books and online resources available that can fill in the details. See the Resources section below for a list.

Ruby’s Features

This section attempts to briefly describe many of Ruby’s features. Please bear in mind that there isn’t enough room in this article to list everything or even fully explain or justify each item.

Object Orientation

Ruby’s object oriented nature is consistent and complete. Everything is an object or a method. Even numbers are objects and even operators are methods. You may see code that looks like it is calling a global function; that is because the top-level code is actually executing within the context of an invisible magical variable representing the script’s main module. This object includes the Kernel module, giving it access to methods such as print, chop, system, sleep, and all the others you might expect from a scripting language. You can write code that looks procedural, but it really isn’t.

Classes inherit via single inheritance but can include multiple modules. Modules provide name spaces and—unlike Java’s interfaces—method implementations. Like Smalltalk but unlike C++ or Objective-C, a metaclass (the class of a class) is a full-blown class itself, with instance variables and methods.

Classes remain “open” so you can add or replace methods for any class at any time. Go ahead: add a method to the String class! Methods can also be added to individual objects, giving them unique behavior.

Introspection and reflection let objects and classes inspect and manipulate their instance variables and methods.

Mark-and-sweep garbage collection (superior to Python’s reference counting scheme) means you don’t have to worry about memory management.

The Language

Ruby’s syntax is simple and consistent. The language has been designed using the Principle of Least Surprise: things work the way you expect them to, with very few special cases or exceptions.

Variable naming rules are simple. The first character determines a variable’s use: @instance_variable, @@class_variable, $global, CONSTANT, :symbol and everything_else. This is different from Perl, where the first character determines the variable’s type. We’ll cover variable and method names in more detail later.

Before going any further, let’s look at some Ruby code. Listing 2 implements a jukebox full of songs to play. This version gets its list of songs by reading the names of the files that iTunes stores in your Music directory. We’ll augment this code later by using the RubyAEOSA module (available separately) to add AppleScript that talks directly to iTunes.

The first line of this script isn’t Ruby code; it’s a magical incantation that tells the shell what program to run to execute the script. Instead of hard-coding the path to the Ruby executable—which could be in /usr/bin/ruby, /usr/local/bin/ruby, or even somewhere else depending upon how it was configured when Ruby was installed—we use the env program to figure out where the Ruby executable lives.

Listing 2: jukebox1.rb


This jukebox reads the filesystem to retrieve the names of all of
your iTunes files. It prints all artists' names and their album
names. It then prints all of the song names from the album you
specify and "plays" each of the songs from that album whose names
start with a vowel.

#! /usr/bin/env ruby
#
# usage: jukebox1.rb [itunes-dir] [artist-name] [album-name]
#
# If itunes-dir, artist-name, or album-name are unspecified,
# default values (defined below) are used.
#
# Normally, I would put each class in its own file.

# This constant holds the name of the iTunes music directory
DEFAULT_ITUNES_DIR =
  "#{ENV['HOME']}/Music/iTunes/iTunes Music"

# A regular expression for making file names palatable
# to the Dir class file name globbing.
FNAME_BAD_CHARS_REGEX = /['"\s]/

# Create a Jukebox class. A Jukebox holds a hash (dictionary)
# whose keys are artist names and values are artist objects.
class Jukebox

  # Declare an instance variable. Declaring it isn't
  # necessary, but by using "attr_accessor" two accessor
  # methods (a getter and a setter) are created for us.
  attr_accessor :artists

  # This method is called by the constructor when a new
  # jukebox is created.
  def initialize
    @artists = Hash.new
  end

  # Return a list of all of the artists' albums.
  def albums
    return @artists.values.collect { | a | a.albums }
  end

  # Load all of the artists, albums, and songs. Provide
  # a default value for the parameter tunes_dir.
  #
  # This isn't the only way to traverse the directory
  # structure, but it will do.
  def load(tunes_dir = DEFAULT_ITUNES_DIR)
    artist_glob = File.join(tunes_dir, '*')
    artist_glob.gsub!(FNAME_BAD_CHARS_REGEX, '?')

    Dir[artist_glob].each { | artist_dir |
      next if artist_dir[0] == ?. # Skip dot files

      artist = Artist.new(File.basename(artist_dir))
      album_glob = File.join(artist_dir, '*')
      album_glob.gsub!(FNAME_BAD_CHARS_REGEX, '?')

      Dir[album_glob].each { | album_dir |
        next if album_dir[0] == ?.

        album = Album.new(File.basename(album_dir))
        song_glob = File.join(album_dir, '*')
        song_glob.gsub!(FNAME_BAD_CHARS_REGEX, '?')

        Dir[song_glob].each { | song |
          song_name = File.basename(song)
          next if song_name[0] == ?.

          # Add the song to the album's song list
          album.songs <<
            Song.new(song_name.sub(/\.mp3$/, ""))
        }
        artist.albums[album.name] = album
      }
      @artists[artist.name] = artist
    }
  end

end

class Nameable

  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def to_s
    return @name
  end
end

class Artist < Nameable

  attr_accessor :albums

  def initialize(name)
    super(name)
    @albums = Hash.new
  end
end

class Album < Nameable

  attr_accessor :songs

  def initialize(name)
    super(name)
    @songs = []
  end
end

class Song < Nameable

  alias_method :title, :name          # Make song respond to
  alias_method :title=, :name=        # "title" and "title="

  def play
    puts "Played song #{title()}"     # See? title() works
  end
end

# ==========================================================

# This code will only execute if this file is the file
# being run from the command line.
if $0 == __FILE__

  DEFAULT_ARTIST = 'Thomas Dolby'
  DEFAULT_ALBUM = 'Astronauts And Heretics'

  fave_artist_name = ARGV[1] || DEFAULT_ARTIST
  fave_album_name = ARGV[2] || DEFAULT_ALBUM

  jukebox = Jukebox.new
  jukebox.load(ARGV[0] || DEFAULT_ITUNES_DIR)

  # Print some stuff

  puts "All Artists:"
  puts "\t" + jukebox.artists.keys.join("\n\t")

  puts "#{fave_artist_name}'s albums:"
  artist = jukebox.artists[fave_artist_name]
  artist.albums.each_value { | album |
    puts "\t#{album.name}"
    puts "\t\t" + album.songs.join("\n\t\t")
  }

  puts "\"Play\" all songs from \"#{fave_album_name}\"" +
    " that start with a vowel"
  album = artist.albums[fave_album_name]

  # Make a new list by rejecting (skipping) songs that
  # do not start with a vowel.
  vowel_songs = album.songs.reject { | song |
    song.name =~ /^[^aeiou]/i
  }
  vowel_songs.each { | song | song.play }

end

In Ruby, variables generally hold references to objects. Thus Ruby is strongly typed but dynamic. Dynamic typing leads to quicker implementation. The benefit becomes apparent when you decide to change your application’s phone numbers from strings to objects of class MyPhoneNumber. You don’t have to change your code everywhere.

Closures are code blocks that remember their context including local variables declared outside the block, the value of self, and more. They provide a way to pass snippets of code to iterators or methods. Listing 3 shows a block being passed to an array’s collect method. It also shows that any local variables defined when the block is created are available to the block.

Listing 3: blocks.rb


In this example, we create an array and a local variable. We then
call the array's collect method, passing a block that uses the
local variable.

#! /usr/bin/env ruby

a = [1, 2, 3]
x = 5

# Pass a block to the collect method. The collect method
# calls the block once for each element in the array,
# passing the element to the block. (The method p calls
# its arguments' inspect method, which is defined in the
# Object class but may be overridden.)
b = a.collect { | elem | elem + x }
p a                # prints [1, 2, 3]
p b                # prints [6, 7, 8]

The keyword yield, when used within a method, calls the block passed to the method. This gives blocks one more nifty use: adding behind-the-scenes behavior before or after the block is executed. For example, when the open method of the built-in File class is given a block argument, it not only passes the opened file into the block but it automatically closes the file when the block exits. Listing 4 shows how that happens.

Listing 4: autoclose.rb


This is how the open method of the File class can automatically
close a file for you. It also shows how you specify default method
argument values. The block isn't declared in the method's
signature. It must be the last thing passed in when the method is
called.

#! /usr/bin/env ruby

# In Ruby, you can add code to any class at any time.
class File

  # The real File.open method is probably a bit more
  # robust than this.
  def File.open(file_name, mode_string = "r", perm = nil)
    f = File.new(file_name, mode_string, perm)
    yield f    # Execute the block, passing the file to it
    f.close()
  end

  # We want to redefine close() but call the original
  # version. We can't just call super() because we are
  # not creating a subclass. Instead, we create a new
  # name for the original version of the method, then
  # use that name inside the redefined method. (This
  # implies that alias_method really clones the method
  # definition instead of simply creating a new name.)
  alias_method :old_close, :close
  def close
    old_close()
    $stdout.puts "file has been closed"
  end

end

File.open(some_file_name) { | f |
  # do something with the file...
}
# "file has been closed" will be printed to stdout.

Traversing a list of values is one of the most common things done in any program. Ideally, while enumerating the elements of a list you don’t care how many things are in the list or what type of object each thing is. In Ruby, enumeration has been abstracted into a mixin module available to all classes and independent of the language syntax. Built-in classes like Array, Dir, Hash, IO, Range, and String all mix in (include) the Enumerable module. If a class includes the Enumerable module and implements one method called each that takes a block argument, it gets all the other methods for free: each_with_index, sort, collect, detect, reject, select, entries, find, grep, include?, map, max, and more.

The Range class represents ranges of integers or strings. (0..3) is syntactic sugar that creates a Range object that will generate the values 0, 1, 2, and 3. (0...3) will generate 0, 1, and 2. Two dots means “include the final value”, three dots means “exclude it”.

A method’s name can be aliased, meaning that more than one name can refer to the same method. Can’t decide between indexes and indices? Can’t remember if it’s array.size or array.length? That’s OK: use either one. The built-in classes make use of aliases to let you decide. We used alias_method in Listing 2 so a song’s name could be referred to as its title.

Ruby’s exception mechanism is similar to Java’s. You can raise exceptions, rescue (catch) them, and re-throw them. Within the rescue block, if you can fix the error you can use retry to jump back to the top of the block. See Listing 5 for an example.

Listing 5: exceptions.rb


An example of exception handling and retrying. This script will
throw the "argument was true" exception the first time it is run.
After the fixing the cause of the exception, it uses "retry" to
cause the begin block to start over.

#! /usr/bin/env ruby

def raise_error_if_true(flag)
  raise "argument was true" if flag
end

def always_executed
  puts "always executed"
end

silly_flag = true
begin
  raise_error_if_true(silly_flag)
  puts "silly_flag was false"
rescue => ex
  $stderr.puts "error: #{ex.message()}; trying again"
  silly_flag = false
  retry
ensure
  # This is exactly like Java's "finally" keyword:
  # code here will always be executed.
  always_executed()
end

# The output of this script will be:
# error: argument was true; trying again
# silly_flag was false
# always executed

In Ruby, boolean expressions are slightly different than what you are probably used to: only nil and false are false; everything else (including the number 0 and empty strings, arrays, and hashes) evaluates to true.

The Rest of the World

Ruby is written in C. Not only does this make Ruby reasonably fast, but it makes integration with existing C code easy. You can extend Ruby with C code or embed a Ruby interpreter within your C code.

Because Ruby is written in C, it has been ported to many different operating systems. Ruby runs under Mac OS 9 and OS X, BSD, Linux, Solaris, BeOS, OS/2, DOS, Windows 95/98/NT/2K, and more. Ruby can also load libraries dynamically on operating systems like Mac OS X that support it.

As with most scripting languages, it is easy to execute commands just like you would from the command line. You can run Unix commands or even launch Mac applications by using the system method or enclosing the command in backquotes. The system method runs the command and returns the exit status; using backquotes returns the output of the command as a string.

In the section Ruby and Mac OS X below we will take a look at some modules that add Apple Event support and Cocoa integration to Ruby.

Ruby is a great language for building Web-based applications. The Web server built in to OS X is Apache, which can be configured and extended through the use of modules (not Ruby modules) such as those used to add PHP, Fast CGI, and—you guessed it—Ruby. The Apache module mod_ruby adds support for Ruby CGI scripts and the eRuby Ruby module adds support for Ruby embedded into HTML, just like PHP or JSP.

Any scripting language used for Web services and page scripting, system administration, and network communications needs some security measures. For example, Ruby allows any string to be executed as code using the eval method. If that string is supplied from someplace outside the script itself such as a file or user input, it may contain destructive or harmful code. The value of the global variable $SAFE determines how much Ruby trusts data stored in its variables. Data is either “tainted” or “untainted”. Tainted data is that which has been supplied externally (for example, use input or file data). By default (when $SAFE == 0), Ruby trusts all data. Larger values of $SAFE cause Ruby to disallow the use of tainted data, prohibit loading programs from unsafe locations, distrust all newly created objects, or prevent modification of untainted objects. Objects can also be “frozen” to prevent further changes.

Ruby implements operating system-independent threading. The good news is that the threading model is highly portable; your threads will work under Jaguar or DOS. The bad news is that Ruby does not yet take advantage of an operating system’s underlying threading, if any.

In the next major release of Ruby, Matz plans to add support for internationalization and multilingualization throughout Ruby. In 1.6.7, the only internationalization support is via the String class and the kconv module, which support a small number of codings, including UTF and Kanji.

The JRuby project is a pure Java implementation of the Ruby interpreter. A module available at the Ruby Application Archive (RAA) provides integration with Objective-C. See “Ruby and Mac OS X” below.

Syntax

Ruby’s syntax is so close to Perl’s and Java’s that it won’t take long to learn. I was writing tiny but useful scripts a few hours after installing Ruby.

Each line of code is a new statement. Semicolons are optional and unnecessary unless you need more than one statement on a line. To continue a line of code on multiple lines, end the line with something that logically continues the line: a comma or operator, for example.

To create a new object, use thing = ClassName.new(arguments). Each class can define the method initialize which is called by the constructor. You can use attr_accessor and friends to automatically create accessor methods (setters and getters). See Listing 6 for an example that defines a simple class with a instance variable definition that uses attr_accessor to automatically generate accessor methods and another that uses attr_reader to generate a getter but no setter.

Listing 6: constructors.rb


An example of constructors and automatically generated accessors.

#! /usr/bin/env ruby

class MyClass

  # attr_accessor creates setter and getter methods for
  # the listed symbols (instance variable names)
  attr_accessor :name

  # attr_reader creates a getter but not a setter.
  attr_reader :the_answer

  def initialize(name="DefaultName")
    @name = name
    @the_answer = 42
  end

  # The method to_s is like Java's toString() method.
  def to_s
    "My name is #{@name}; the answer is #{@the_answer}."
  end
end

my_thing = MyClass.new("Ruby")
my_thing.name = 'New Name'

# If this line was uncommented, an exception would be
# thrown stating "undefined method `the_answer='"
# my_thing.the_answer = 3

puts my_thing.to_s

As mentioned previously, a variable’s use is determined by the first character of its name: @instance_variable, @@class_variable, $global, CONSTANT, :symbol, and everything_else. Note that Ruby class and module names must start with capital letters. This implies they are constants.

By convention, method and variable names are all lowercase and words are separated_by_underscores. This is not enforced by the parser. Another convention that may look unfamiliar at first is the use of ? and ! in method names. Method names that end with ? return boolean values. Examples include Object.nil? and Array.empty?. Method names ending with ! modify the receiver. For example, String.strip returns a new copy of the receiver with leading and trailing whitespace removed; String.strip! modifies the receiver by removing leading and trailing whitespace.

A Symbol is a unique identifier. All occurrences of the same symbol share the same instance. Symbols are used to represent method and instance variable names and can also act as unique values for constants (think C’s enums). Symbols have both string and integer representations.

Parentheses around method arguments are optional. Beware, though: leaving them out can occasionally cause unexpected results. Instead of memorizing complex rules, do what I do: when in doubt, use parentheses.

Arrays are created using either a = Array.new or a = [1, 'foo', thing]. The new method takes two optional arguments: array size and initial value. Hashes (also called hash maps, associative arrays, or dictionaries) are created using either h = Hash.new or h = {'a' => 1, 'b' => 'foo', 'c' => thing}. The new method takes one optional argument: the default value for undefined hash keys.

String constants can be contained within double or single quotes. When surrounded by double quotes, the contents of the string are interpolated. All occurrences of "#{x}" within the string are replaced by the value of x. Note that x may be anything: a variable, an expression, or an entire block of code. As a shortcut, "#{$var}" may be written as "#$var" and "#{@instance_var}" as "#@instance_var".

The [] and []= (assignment) methods of String can take an integer and return a single character, take two integers (starting index and length) to return a substring, or even take a regular expression and return the substring matching that expression. See Listing 7 for some examples.

One important note: there is no character class in Ruby. When you retrieve a character from a string you get back its integer value. To retrieve a one character string you have to ask for a string of length one. See Listing 7.

To write a character constant, use ? before the character. For example, ?A is an upper-case A.

Listing 7: string_accessor.rb


String accessor examples. Additionally, the String class provides
a rich series of methods for string manipulation.

#! /usr/bin/env ruby

s = "abcdefg"
s[0]            # => 97, the ASCII value of 'a'
s[0,1]          # => "a"
s[1,2]          # => "bc"
s[/b.*f/]       # => "bcdef"
s[0..3]         # => "abcd"
s[0...3]        # => "abc"
s[-4..-1]       # => "defg"

s[0] = ?x       # s == "xbcdef"
s[/b.*f/] = "z" # s == "xzg"

Ruby’s control structures will be familiar to users of most languages. There’s nothing surprising, except perhaps the use of elsif instead of else if. Like Perl, you can write do_this() if that or do_that() unless this.

The case structure (called “switch” in Java and C) is interesting. The branches use the === method to compare the target with any other kind of object: a number, a regular expression, or even a class. See Listing 8.

Listing 8: case.rb


The case statement uses the === method to perform comparisons.

#! /usr/bin/env ruby

x = 'abcdefg'
case x
when 'xyzzy', 'plugh'            # Compare with constants
  # You can specify multiple potential matches in the
  # same "when" clause.
  puts "xyzzy or plugh"
when /def/                       # Compare with regex
  # Matches; this code will execute because the previous
  # comparison failed.
  puts "found 'def'"
when String                      # Compare x's class with String class
  # This matches, but the previous comparison executed
  # already.
  puts "It's a String, all right"
else
  # The default case, when all others fail
  puts "oops"
end

Listing 9 contains some examples of enumerating over objects’ contents. Since the Enumerable module can be included in any class and only requires implementation of one method (each), it is easy to remember how to use and easy to add to your own classes.

Listing 9: enumerating.rb


Enumerating over an array, a hash, a string, and a
user-defined class.

#! /usr/bin/env ruby

array = [1, 2, 3]
hash = {"a" => 1, "b" => 2, "c" => 3}
file = File.open($0, "r")        # $0 is this script's name
string = "abc"

# Notice how many different classes access their contents
# the same way because they all import Enumerable and
# implement the "each" method.
array.each { | elem | puts elem }
hash.each { | key, val | puts "#{key} => #{val}" }
file.each { | line | puts line }
string.each { | line | puts line }

# each_with_index is defined in the Enumerable module
array.each_with_index { | elem, i |
  puts "array[#{i}] = #{elem}"
}

# The String class adds the each_byte method, since
# String.each iterates over lines of text, not characters.
# The output will be a list of integers.
string.each_byte { | c | puts c }

# The include? method is defined in the Enumerable module
puts "yup" if array.include?(42)

# Now let's define our own class and make it enumerable.
class TrainTrip

  include Enumerable

  def initialize
   @stops = %w(Paris London Boston Tokyo Kiev)
  end

  # Implement the "each" method when including the
  # Enumerable module. By doing so, all the other
  # methods in that module (each_with_index, sort,
  # collect, detect, reject, select, entries,
  # find, grep, include?, map, max, and more) are
  # available for free.
  #
  # If the last parameter in an argument list starts
  # with an ampersand, then if a block is passed to
  # the method Ruby will convert it into a Proc
  # object.
  def each(&block)
    @stops.each(&block)
  end

end

trip = TrainTrip.new
trip.each_with_index { | stop, i | puts "#{i+1}: #{stop}" }

# Because Enumerable takes care of everything else, we
# get lots more behavior for free.
trip.include?("London")        # => true
trip.sort.each { | stop | puts stop }

file.close

Classes and Libraries

Ruby comes with an impressive and useful library of classes and modules. The Ruby Application Archive is the best place to find and publish additional Ruby libraries and applications. When I counted in February of 2003 there were over 800 entries in the archive. They include libraries for Mac OS X, database access, networking, WWW and CGI programming, XML, unit testing, documentation, cryptography, AI, graphics, editors, GUI creation, games, and more.

Built-In Classes

The classes and modules that come with Ruby include value holders (Array, String, Hash, Numeric, Range, and Time), I/O classes (IO, File, and Dir), OO classes (Object, Class, Module, Struct, and the ObjectSpace module), operating system integration classes (Thread, ThreadGroup, and the Kernel module), regular expression classes (Regexp and Match), and more.

The hierarchy of numeric classes (Numeric, Float, Fixnum, and Bignum) provides double-precision floating point and infinite-precision integer arithmetic. The Math module provides sin, sqrt, log, and friends.

Objects can be marshalled—converted into byte streams and back—via the Marshall module. The library module named drb (Distributed Ruby) found on the Ruby Application Archive combines this ability with the networking library to provide an easy-to-use framework for writing distributed Ruby applications.

Libraries

The standard Ruby distribution comes with a number of libraries including networking, XML parsing, date manipulations, persistent object storage, Tk (a cross-platform GUI infinitely less pretty than Aqua), mutex thread synchronization, MD5 (cryptography), debugging, matrix math and complex numbers, design patterns like observer/observable and delegation, OpenGL, and much more.

The standard Ruby libraries are found in /usr/lib/ruby/1.6 (or if you’ve installed version 1.8 yourself, in /usr/local/lib/ruby/1.8 and used the default install directory). Additional libraries you download and install usually place themselves in /usr/lib/ruby/site_local/1.6.

Ruby and Mac OS X

Fujimoto Hisakuni has written three Mac OS X Ruby bindings: RubyAEOSA, RubyCocoa, and an Objective-C binding. They come with plenty of example code. All three are available within the same package. Download RubyCocoa version 0.3.2 or higher from the RAA. The download is a disk image (“.dmg”) file which contains, among other things, a “.pgk” package file. Install the package and you are ready to use the three libraries. Version 0.3.2 is for Jaguar only; it won’t work with earlier versions of Mac OS X. If you are running an earlier version, download an earlier version of the libraries and follow the included installation instructions.

RubyAEOSA lets you send create and send Apple Events, run AppleScript code, and retrieve the results from either. RubyCocoa allows your scripts to use Cocoa objects. You can even write small Cocoa applications using Ruby and Interface Builder.

Let’s use some AppleScript to let the jukebox from Listing 2 talk directly to iTunes to retrieve song information and play a song. See Listing 10. Two warnings: first, this AppleScript takes a few seconds to execute if you have a lot of music, whether it is run from Script Editor or this Ruby script. Second, for some reason this AppleScript code kept trying to launch the OS 9 version of iTunes. I had to move the OS 9 iTunes folder into the Trash and reboot before it would launch the OS X version. Even then, it started trying to launch the OS 9 version after a while. I emptied my trash. I’m no AppleScript expert, so all this was probably my fault.

Listing 10: jukebox2.rb


Add some AppleScript to the jukebox so it can talk to iTunes.
Lists artist's albums and tells iTunes to play the first song
from the specified album whose name starts with a vowel.

#! /usr/bin/env ruby
#
# usage: jukebox2.rb [artist-name] [album-name]
#
# Add some AppleScript to the jukebox so it can talk to
# iTunes. Lists artist's albums and tells iTunes to play the
# first song from the specified album whose name starts
# with a vowel.
#
# If artist-name or album-name are unspecified, default
# values (defined below) are used.

require 'osx/aeosa'
require 'jukebox1'            # The original jukebox code

# Here's the AppleScript we will use to gather song
# information.
LOAD_SCRIPT = <<EOF
set all_tracks to {}
tell application "iTunes"
  tell source "Library"
    tell playlist "Library"
      set the track_count to the count of tracks
      repeat with z from 1 to the track_count
        tell track z
          set all_tracks to all_tracks & \{\{name, artist, album\}\}
        end tell
      end repeat
    end tell
  end tell
end tell
all_tracks
EOF

# Since class definitions never "close", we can easily
# define new methods or overwrite existing ones.

class Jukebox

  def load
    ret = OSX.do_osascript(LOAD_SCRIPT)
    all_tracks = ret.to_rbobj

    all_tracks.each { | track_ae |
        song_name, artist_name, album_name =
          track_ae.collect { | ae_obj | ae_obj.to_s }

        # See if we already have this artist in the
        # jukebox. If not, add it.
        artist = @artists[artist_name]
        if artist.nil?
          artist = Artist.new(artist_name)
          @artists[artist_name] = artist
        end

        # See if this artist already has this album.
        album = artist.albums[album_name]
        if album.nil?
          album = Album.new(album_name)
          artist.albums[album_name] = album
        end

        # Add the song to the album
        album.songs << Song.new(song_name)
    }
  end

  def play(album, song)
    script = "tell application \"iTunes\"\n" +
      "tell source \"Library\"\n" +
      "tell playlist \"Library\"\n" +
      "play (tracks whose name is \"#{song.name}\"" +
      " and album is \"#{album.name}\")\n" +
      "end tell\n" +
      "end tell\n" +
      "end tell\n"
    OSX.do_osascript(script)
  end
end

# ==========================================================

# This code will only execute if this file is the file
# being run from the command line.
if $0 == __FILE__

  DEFAULT_ARTIST = 'Thomas Dolby'
  DEFAULT_ALBUM = 'Astronauts And Heretics'

  fave_artist_name = ARGV[0] || DEFAULT_ARTIST
  fave_album_name = ARGV[1] || DEFAULT_ALBUM

  jukebox = Jukebox.new
  jukebox.load()                # Use AppleScript

  # Print some stuff

  puts "All Artists:"
  puts "\t" + jukebox.artists.keys.join("\n\t")

  puts "#{fave_artist_name}'s albums:"
  artist = jukebox.artists[fave_artist_name]
  artist.albums.each_value { | album |
    puts "\t#{album.name}"
    puts "\t\t" + album.songs.join("\n\t\t")
  }

  puts "Play the first song from \"#{fave_album_name}\"" +
    " that start with a vowel"
  album = artist.albums[fave_album_name]

  # Make a new list by rejecting (skipping) songs that
  # do not start with a vowel.
  vowel_songs = album.songs.reject { | song |
    song.name =~ /^[^aeiou]/i
  }

  # Play the first song we found
  jukebox.play(album, vowel_songs[0])

end

Finally, Listing 11 lets your computer declare out loud the name of its new favorite language.

Listing 11: speak.rb


Tell me: what's my favorite language?

#! /usr/bin/env ruby

require 'osx/cocoa'
include OSX

def speak(str)
  str.gsub!(/"/, '\"')        # Put backslash before quotes
  src = %(say "#{str}")
  # Call Objective-C code
  script = NSAppleScript.alloc.initWithSource(src)
  script.executeAndReturnError(nil)
end

speak "Ruby is my favorite language!"

Conculsion

I hope this article has given you enough of a taste of Ruby’s features, power, and elegance that you want to explore it more. Apple liked it enough to include it with Jaguar. May you find it half as fun and useful as I have.

Resources

Books

Thomas, David and Andrew Hunt. Programming Ruby. Addison Wesley,

  1. This book, known also as “The Pickaxe Book” for its cover picture, was the first English book on Ruby. It is an excellent tutorial and reference. Published under the Open Publication License, the entire book is available online. I highly recommend you buy a copy.

Matsumoto, Yukihuro. Ruby in a Nutshell. O’Reilly, 2002. This is a “Desktop Quick Reference” based on Ruby 1.6.5.

Feldt, Robert, Lyle Johnson, Michael Neumann (Technical Editor). Ruby Developer’s Guide. Syngress, 2002.

Fulton, Hal. The Ruby Way. Sams, 2002.

Internet Resources

The Ruby home page. The Ruby Application Archive (RAA) lives there as well.

The Usenet news group comp.lang.ruby and the mailing list ruby-talk are synchronized (using Ruby code, of course). comp.lang.ruby is where I go first if I can’t find something in the manuals or books. Matz or other expert Ruby programmers often answer questions there. See the Ruby Web site for mailing list details. Ruby Talk contains a searchable archive of the list. The community of Ruby users is among the most friendly and helpful I have seen.

Ruby Central is hosted by the Pragmatic Programmers (Dave Thomas and Andy Hunt, authors of Programming Ruby).

Ruby Garden is an excellent collection of Ruby news, links, polls, and is the home of the Ruby Garden Wiki. (A Wiki is a Web site with user-editable pages. They’re great for collaboration.)

William Tjokroaminata’s Web page Things that Newcomers to Ruby Should Know contains a number of tips for new Ruby programmers.

The JRuby project. DataVision, my database reporting tool project, is written in Java but uses Ruby as its formula and scripting language.

About the author…

Jim Menard is a senior technologist with twenty years of experience in development, design, and management. Like so many of us, Jim is an ex-dot-com CTO and a consultant. A language maven, Jim loves everything about Ruby. He has developed Open Source projects such as NQXML (the first pure-Ruby XML parser), Rice, DataVision (a Java GUI report writer with Ruby scripting), and TwICE. You can contact him at jim@jimmenard.com.