devxlogo

10 Minutes to Your First Ruby Application

10 Minutes to Your First Ruby Application

o 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:

  1. Demonstrate some features of Ruby.
  2. 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 walkthrough 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.

The example task is simplifying file launching. Given a text file (maybe a Ruby source code file), suppose you want to create a way to launch it in some associated application from the command line. And you want to launch it without having to keep track of file types and application associations. Yes, Windows already does this, but your application will have additional features that go beyond this simple behavior.

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/local/bin/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 “Ruby?A 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 nothing?unless 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/local/bin/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.

Secondly, 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     endend

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" endif ARGV.empty?  help  exitelse  app_map = {     'html' => 'firefox',     'rb' => 'gvim',     'jpg' => 'gimp'  }  l = Launcher.new( app_map )  target = ARGV.join( ' ' )  l.run( target )end

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/local/bin/rubyrequire 'launcher'# Script to invoke launcher using command-line argsdef help  print "   You must pass in the path to the file to launch.  Usage: #{__FILE__} target_file" endunless ARGV.size > 0  help  exitelse  app_map = {     'html' => 'firefox',     'txt' => 'gvim',     'jpg' => 'gimp'  }  l = Launcher.new( app_map )  target = ARGV.join( ' ' )  l.run( target )end

Note the extra line of code near the top:

require 'launcher'

You need this line to make your Launcher available to the current script. 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.)

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}" )  endend

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}':
#{$!}"     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 

There's a lot going on here, but 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.

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.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist