10 Minutes to Your First Ruby Application
2012-11-01 01:20:07 +0000
By James Britt
2012-11: The code and article has been updated to fix some errors and to work with Ruby 1.9.3
At the time it was written, and because of where it was originally published, it was targeted (more or less) at people working on Windows, so there may be a few unstated assumptions in the text.
On the other hand, the command-line examples seem to reflect a unix shell, no doubt because that's where the bulk of the content was created.
Apologies in advance for any confusion.
There's no better way to experience the elegance and power of Ruby than to fire up your code editor and start writing Ruby code. Create a small, useful Ruby application, and along the way, you'll learn what makes the language tick.
So you've discovered the grace and power of Ruby and you're ready to explore the subtle but important ideas behind its elegance. Follow this tutorial to create a small, useful Ruby application. As Ruby is primarily an object-oriented language with classes and objects, you can jump right in and create a class to encapsulate behavior. The instructions begin with a simple version of the application, and then expand it. Along the way, you will learn what makes Ruby tick.
The example application will serve two purposes:
- Demonstrate some features of Ruby
- Do something useful in the process
A word on the title: Were you to write this code yourself, assuming some moderate Ruby knowledge, it probably wouldn't take more than 10 minutes. Once you learn how Ruby works and understand what sort of code it enables, you'll find that you can whip up useful utilities in short order. Of course, a walk-through of such code will take a bit more than 10 minutes if you're new to the language.
What You Need |
---|
This tutorial assumes that you already have a current version of Ruby installed, and you have a code editor handy. You don't need a fancy IDE to code in Ruby; Vim, Emacs, and TextMate are great choices. NetBeans and Eclipse work fine as well. |
Target Problem: Simplifying File Launching
Ruby is primarily a text-based, command-line-oriented language. Some GUI libraries are available, as well as multiple Web application frameworks, but exploring GUI development with Ruby is beyond the scope this article. The goal here is to write something that works from the command line.Version 0: The Launcher Code
First, create a sparse Ruby file. Ruby files end with.rb
and have the pivotal line that defines the path to your Ruby interpreter up top. Call the file launcher.rb
:
#!/usr/bin/env ruby
# Example application to demonstrate some basic Ruby features
# This code loads a given file into an associated application
class Launcher
end
Notice you can use a pound sign (#) to start a line-level comment. Everything to the right of the # is hidden from the interpreter. Ruby has a means for commenting multiple lines of code, too. Class names begin with a capital letter; classes are constants, and all Ruby constants start with a capital letter. (For a more complete overview of Ruby syntax, please see "RubyA Diamond of a Programming Language?", Part 1 and Part 2.)
While this code seemingly does nothing, it is executable. If you're playing along at home, you should see that your copy of the code executes. A simple way to run a Ruby script is to simply call the ruby interpreter and pass the name of the file, like this (see Sidebar 1. Instructions for Executing launcher.rb in Unix and Windows):
$ ruby launcher.rb
When you run the file, you should see nothingunless there's an error of some sort in the code. So, nothing is good. It doesn't mean nothing is happening; when the ruby interpreter parses your file, it encounters your class definition and makes it available for creating objects. The following code adds the class definition to your code:
#!/usr/bin/env ruby
# Example application to demonstrate some basic Ruby features
# This code loads a given file into an associated application
class Launcher
end
launcher = Launcher.new
The code first creates a variable (
launcher
) that is assigned a reference to a new instance of the class Launcher. You do not have to declare the type of the variable. Ruby uses strong, dynamic typing, and variables can hold references to objects of any type. Pretty much everything in Ruby is an object, including strings, numbers, and regular expressions. Each of these has a formal creation method (e.g., String.new
), but Ruby tries to make it easy and fluid to work with the common cases.
Second, Ruby creates the object instance by invoking
new
on your Launcher class. New
is a class method; it's analogous to constructor methods in Java. Of course, an empty object won't get you far, so you must add some behavior.
Adding Behavior
The essence of your application takes a given file name and passes it to an associated application for processing of some sort. The launcher code will need to know how to do this mapping, so when you create an instance of a Launcher class, you must pass in some sort of mapping. You've seen that you can use the class method
new
to create an instance of a class. To create an instance that starts life with some set of data, you can pass in arguments to new
. To handle this, you of course will have to add some code to Launcher:
def initialize app_map
@app_map = app_map
end
You define methods in Ruby using the
def
keyword, followed by the method name, and then the augment list, if any. The argument list is in parentheses for clarity, though Ruby will allow you to omit them when the meaning of the code is unambiguous (see Sidebar 2. Why You Add initialize Method When Passing Arguments to new
Method).
It's worth noting then that Ruby objects begin life with assorted built-in behavior. You can use these as is, or opt to override them.
Instance Variables
Your initialize method takes one argument,
app_map
. Again, as with the earlier variable, you do not give the types of method arguments. You just say that the method takes one argument (app_map
), and in the body of the method this argument gets assigned to the variable @app_map
. The @ symbol indicates that the variable is an instance variable (i.e., it is available to all the code in this object). You create this instance variable when you create your object, and it will be available to any other methods you add to your code.
To have your application execute a given file using the associated application, drop some more code into it:
class Launcher
def initialize app_map
@app_map = app_map
end
# Execute the given file using the associate app
def run file_name
application = select_app file_name
system "#{application} #{file_name}"
end
# Given a file, look up the matching application
def select_app file_name
ftype = file_type file_name
@app_map[ ftype ]
end
# Return the part of the file name string after the last '.'
def file_type file_name
File.extname( file_name ).gsub( /^\./, '' ).downcase
end
end
The method
run
takes a file name as its argument, passes it to select_app
to find out which application to execute, and then uses Ruby's system method to invoke that application, passing the file name. The system
method simply kicks the given command into a sub-shell. While select_app
takes the file name, calls file_type
to get a 'normalized' file extension, and then uses that as a key into @app_map
to see which application to run.
Finally,
file_type
takes the file name and uses a class method on Ruby's File class to get the extension. The string returned by extname
includes the period (.) that precedes the file extension. You don't need that, so the code uses gsub
(or global substitute) to strip it; it then converts what remains to all lowercase letters with downcase
.
For compactness, all these method calls are chained together. The string returned from File.extname is the receiver of the gsub request; the string returned from gsub then becomes the receiver of the call to downcase.
The example code so far has used objects that you expect to be Strings and Hashes, but what you really care about is that these objects will respond to particular messages in an appropriate way. (Before delving into how to call your shiny new object, see Sidebar 3. A Few Words About Objects, Types, and Behavior.) For such a small application, the subtlety and power of an object system based on messages and run-time behavior may not be critical, but it is important to understand this as you go on to write larger Ruby applications.
Rounding Out Version 0
Finish up this first version by putting it to use. You can add the following code to the end of the file to create an instance of Launcher and use it to run an application:
def help
print "
You must pass in the path to the file to launch.
Usage: #{__FILE__} target_file
"
end
if ARGV.empty?
help
exit
end
app_map = {
'html' => 'firefox',
'rb' => 'gvim',
'jpg' => 'gimp'
}
l = Launcher.new app_map
target = ARGV.join ' '
l.run target
You can download this code here.
The method help will render instructions if needed. ARGV is the argument vector; it is a built-in Ruby object that holds all the parameters passed to your program. If it's empty, then your program has nothing to work with, so it displays the help and exits. Otherwise, it creates a hash object and assigns it to the variable app_map.
The { ... } notation is Ruby's literal syntax for creating a Hash object. You could have used Hash.new, but it's verbose. Using the literal notation, you map hash keys to values using =>. The hash is used to populate your Launcher instance, while the command-line arguments are collected into a single string stored in the variable target, which is passed into run.
Before trying this code, you need to change the application values used in app_map so that they refer to the proper executable. Assuming you have "rb" mapped to a text editor, you can try the code like this:
$ ruby launcher.rb launcher.rb
This should open your source code in your editor.
Bulking Up to Version 1 with Dynamic Loading
So far, so good with Version 0, but you can do better. Rather than having a simple, direct mapping of file types to the application, you could map file types to execution handlers. That is, you can define code for your file types that can then decide which application to run, and with which arguments, depending on additional command-line arguments.For example, if you are doing web development and have created an HTML file, you most often want to view it in a browser. So your application as it is works OK. But sometimes you want to view it using a particular browser. Right now, Launcher only allows a single application association. What you may want is the ability to launch myfile.html in the Opera web browser:
$ ./launcher myfile.html opera
Or you my want to perform some syntax checking on the HTML:
$ ./launcher myfile.html syntax
In other words, you want to add some smarts (see Sidebar 4. The Smarts Behind Launching Logic).
Dynamic Loading
To add those smarts, you will change your program so that you can associate file types with Ruby code rather than associating a particular application. That Ruby code will handle the launching logic, allowing you to decide just how clever to be when launching an application for a given file type (see Sidebar 5. Dynamic Class Loading with Defined Custom Ruby Classes).
Before doing this, make one small change. Having all your code in one place is handy, but it's not a good practice for anything but the smallest apps. For the sake of better organization, split out the general class code from the code that interacts with the user. Do this by creating a file, go.rb, and moving all but the actual Launcher code into that file (i.e, that last chunk of code you just added):
#!/usr/bin/env ruby
require 'launcher'
# Script to invoke launcher using command-line args
def help
print "
You must pass in the path to the file to launch.
Usage: #{__FILE__} target_file
"
end
unless ARGV.size > 0
help
exit
end
app_map = {
'html' => 'firefox',
'txt' => 'gvim',
'jpg' => 'gimp'
}
l = Launcher.new app_map
target = ARGV.join ' '
l.run target
Note the extra line of code near the top:
$:.unshift '.'
require 'launcher'
You need these line to make your Launcher available to the current script.
[EDIT: Since Ruby 1.9, ruby does not automatically include the current directory on the require look-up path. $:.unshift '.' takes the current folder and adds it to the statr of the array of places your ruby app will look for files to require.]
The require method looks for a file matching the given string. The file extension is omitted, so Ruby first will assume you want a .rb file but also will look for a compiled library (e.g., .so) if it doesn't find a Ruby file. (Ruby searches a pre-defined load-path, which includes the current directory, so if you keep launcher.rb in the same place as go.rb, you're good. If you move it, you have to be more explicit about were Ruby can find it.)
Downloads: go.rb and launcher.rb
Writing a Handler Class
Now that you have a simple framework for routing file names to Ruby code, create a handler class for HTML files. The class needs to implement a run method that accepts at least one argument for the target file name, and an optional array of additional parameters. The class name must be Html in a file named html.rb, and placed in a handlers subdirectory:
class Html
DEFAULT_BROWSER = 'firefox'
def run file, args
if args.empty?
system "#{DEFAULT_BROWSER} #{file}"
else
dispatch_on_parameters file, args
end
end
def dispatch_on_parameters file, args
cmd = args.shift
send "do_#{cmd}", file, args
end
def do_opera file, args=nil
system "opera #{file} #{args}"
end
def do_konq file, args=nil
system "konqueror #{file} #{args}"
end
end
The code defines a constant for a default browser. In the absence of any extra arguments, then, you can have the target file launched in Firefox. (Note that you may have to change this so that it defines an executable command. On my Ubuntu machine I can run firefox with no explicit path and have a browser come up. On Windows, for example, the full path to the executable may be needed.)
If there are additional arguments, run calls out to dispatch_on_parameters, which extracts the first item from the args array and uses it to dynamically construct a message string. The send method is built in to all Ruby objects. It allows you to explicitly send a message to an object. When used by itself (as you are doing here), the receiver object is assumed to be the current object. So the code is sending a message to itself.
You prepend do_ to the actual argument value as a safeguard against method name collision. (For example, if the first argument were exit, you probably would not want to invoke Ruby's exit method. You'd call do_exit, which would then decide what the correct behavior should be).
This handler code has some fairly trivial examples of possible parameter handling. As is, you can launch a target HTML file in either some default browser or specify a particular browser:
$ ./go index.html opera
$ ./go index.html konq
A Little Overtime for Coolness
You've received an educational and practical example, but can you push things a little further? Of course you can. Mind you, this will take you past the 10-minute mark, but it should be worth it.The standard Ruby distribution includes a wealth of libraries for all sorts of tasks. One of the most interesting is REXML, an XML parser written in pure Ruby. Developer Sean Russell wrote REXML to allow the manipulation of XML using a Ruby-style API rather than the usual W3C DOM API. Before too long, Sean's work became part of the Ruby standard library.
For the sake of simplicity, your HTML files in this example must use XHTML because REXML handles only XML. (There are very good Ruby tools for processing near-arbitrary HTML, one being Hpricot. However, they require installing additional libraries, the explanation of which is beyond the scope of this article.) Trusting that you are working with well-formed XHTML source, you can have your HTML handler do some file analysis. Add this code to the end of your Html class and you'll be able to run some simple reports on your XHTML:
def do_report file, args=nil
require 'rexml/document'
begin
dom = REXML::Document.new( IO.read( file ) )
if args.empty?
puts basic_xhtml_report( dom )
else
puts report_on( dom, args.first )
end
rescue Exception
warn "There was a problem reading '#{file}':\n#{$!}"
end
end
def report_on dom, element
els = dom.root.elements.to_a "//#{element}"
"The document has #{els.size} '#{element}' elements"
end
def basic_xhtml_report dom
report = []
css = dom.root.elements.to_a '//link[@rel="stylesheet"]'
unless css.empty?
report << "The file references #{css.size} stylesheets"
css.each do |el|
file_name = el.attributes['href']
file_name.gsub! /^\//, ''
unless File.exist? file_name
report << "*** Cannot find stylesheet file '#{file_name}'"
end
end
end
js = dom.root.elements.to_a '//script'
unless js.empty?
report << "The file references #{js.size} JavaScript files"
js.each do |el|
file_name = el.attributes['src']
file_name.gsub! /^\//, ''
unless File.exist? file_name
report << "*** Cannot find JavaScript file '#{file_name}'"
end
end
end
report.join "\n"
end
There's a lot going on here, but the key method is do_report. The code creates a REXML Document object and assigns it to dom. If there are no extra arguments, you get back a basic report. Otherwise, the code does some cursory examination of a particular element.
The report_on method takes a document argument and an element name, and uses REXML's XPath features to find out how often that element is used. Although it's rudimentary, it certainly can serve as a demonstration and starting point for you to keep hacking.
The basic_xhtml_report method is similar, but focuses on a particular set of elements. It uses REXML to find all the CSS and JavaScript references, and then uses the File class to check that the referenced files exist. Again, not deep, but adding additional logic makes for a nice project.
You can download these files from here,
Clean, Expressive Code with Minimal Scaffolding
You now should have a better understanding of some of the features that make Ruby so special, namely:- Ruby is primarily an object-oriented language, where a key concept is objects responding to messages.
- Ruby uses strong, dynamic typing, where the notion of "type" is based on what an object can do more than on a particular class name or inheritance hierarchy. An object's behavior is not confined to a literal mapping of messages to methods, and behavior may be constructed dynamically at run time.
- Ruby classes are open; you are free to alter their behavior for what you deem appropriate for a given application.
This combination of open classes and dynamic behavior enables you to write clean, expressive code with a minimum of boilerplate scaffolding. Ruby gets out of the way and lets you get coding.
This article was originally published on DevX.com