AddThis Social Bookmark Button

Print

Ruby/Tk Primer: Creating a cron GUI Interface with Ruby/Tk

by Christopher Roach
06/25/2004

This article is the first in a three-part series that teaches you how to use Ruby and Tk on your Mac OS X system. During the course of this tutorial, I hope to convince you of the advantages of using Ruby and the Tk toolkit by creating a program that will act as a GUI-based front end to the cron daemon.

In the first half of the series, we'll create the back end of our program. This will be an array-based class that allows us to load/save cron jobs from/to a text file whose contents will be added to the cron schedule. By doing so, I hope to provide a primer to Ruby for those of you who are unfamiliar with the language.

The next article will concentrate on creating the GUI for the back end we create in this article. The GUI we create will be implemented with the Tk GUI toolkit, developed by John Ousterhout in the late 80s as a complement to his Tcl scripting language. Both of the technologies we're using here have been ported to nearly every platform imaginable, so the skills you develop in these articles, as well as the program itself, will be portable to nearly any system you can imagine.

Regardless of whether you're interested in programming with Ruby or Tk or both, or even if your just interested in getting a free program that makes cron a bit more user-friendly, I hope you'll find these articles useful and entertaining. So, without any further ado, let's get down to the real nitty-gritty and learn a little bit about Ruby.

What Is Ruby?

Related Reading

Ruby in a Nutshell
By Yukihiro Matsumoto

Ruby is a dynamic object-oriented language created by Yukihiro "Matz" Matsumoto in 1993 as a replacement for Perl. The Ruby language incorporates many of the best features of other existing languages to make it a very powerful and easy-to-use language.

Like Smalltalk, Ruby is a pure object-oriented language, meaning that everything in Ruby is an object. Thus, messages can be sent to a static string like so: "Hello World!".length(). Also, Ruby is dynamically typed, just like Smalltalk, so it is not necessary to declare the variables in your program. Another feature shared by Smalltalk and Ruby (and other languages, such as Java) is the use of garbage collection to return memory that was previously allocated back to the system. This essentially gets rid of the infamous memory leaks associated with other languages such as C and C++.

Similar to Perl and Python, Ruby is interpreted and can be used to develop short and powerful programs that automate tasks on your system. Because of this last feature and a few others, such as dynamic typing, Ruby is often referred to as a scripting language. This is a bit of a misnomer, since scripting languages are usually considered to be pseudo-languages and not quite as powerful as their full-fledged siblings.

However, unlike most scripting languages, including Perl, Ruby was designed from the ground up to be object-oriented. It is for this reason that Matsumoto has preferred to call his language a "dynamic object-oriented language" rather than describe it as a scripting language. Regardless of whether you refer to Ruby as a programming language or as a scripting language, it still remains that Ruby can be used to develop quick, short, powerful scripts as well as easily maintainable, purely object-oriented, full-fledged applications. I like to think of Ruby as an extremely portable, all-in-one, Swiss army knife for any of your computing tasks. With all of these features leaving you drooling to learn more, why are we wasting time talking? Let's get our hands dirty and write some code.

Hello, Ruby!

Since I'm not one to go against tradition, its only fitting that our first program be the ubiquitous "Hello World" program. Start by opening up your favorite editor (I tend to be rather partial to Emacs) and typing in the following code:


puts "Hello, World!"

Save the file with whatever name you like and type ruby filename into the command prompt of your terminal application. Remember that filename should be the name you chose for your file. Press Return, and voilà! You have written your very first Ruby program.

Let's dissect it, shall we? What we have done here is call the puts method and pass in the string "Hello, World!" as an argument (feel free to surround the string with parentheses if it makes you feel better; however, in Ruby they are not necessary). The puts method just prints a given string to the standard output (in this case, the command line) and tacks a new line onto the end.

Another method, print, does just the same, but it excludes the new line. If you noticed in that explanation I referred to puts as a method, but doesn't it look like its just a function call? After all, I don't see an object reference in front of the puts method, and of course you remember that I claimed that Ruby was a pure object-oriented language earlier, don't you? Well, I didn't lie to you. What is essentially happening here is the same as calling self.puts("Hello, World!").

Self is a reference to the object in which the method resides. It's similar to the this reference used in C++. So what's the reference self referring to? In all Ruby programs, everything executes within the context of a top-level object. Since all objects inherit from the main Object class, which includes methods from the Kernel module (which, incidentally, is where the puts method can be found), every object has access to methods such as puts and print. The call to puts is really a call to the puts method associated with the top-level object in which our program is executing. So even though it looks like a function call in a procedural language, we are really calling a method from an object.

We've now written and executed our first Ruby program. Let's move on to something a little tougher and begin developing our cron back end. We'll start by learning how to create a class and all of its attributes and actions.

Creating Classes In Ruby

Classes in Ruby are created by using the keywords class and end. Let's begin our program by creating a new file called CronJobMgr.rb and typing in the following code to begin our class:


class CronJobMgr
end

Take notice of the character case in the code above, Ruby is a case-sensitive language. Also, it uses the first characters of names to identify their usage. In Ruby, class names and constants begin with a capital letter, instance variables begin with an "at" symbol (@), class variables with two of them (@@), global variables with a dollar sign ($), and local variables and method names with a lowercase letter. In this sense, Ruby seems to be a little more like Perl. However, Perl's identifiers are used more for data-typing, whereas Ruby's are more for scope.

With a class now defined, shall we move onto adding some attributes and actions to it, so that it can accomplish something more than just being created? Let's kill two birds with one stone and create a method and all of the class's attributes at the same time. Since Ruby is dynamically typed, we can do this all in one step; otherwise, we would have to declare our variables before using them as we have to in languages such as Java. This is truly one of Ruby's strengths since it allows the user to write powerful object-oriented scripts in a short amount of time and with very little code. Open up that file again and add the following method to the CronJobMgr class:


def initialize(minute, hour, day, month, weekday,
    command)
	@minute = minute
	@hour = hour
	@day = day
	@month = month
	@weekday = weekday
	@command = command
end

It's easy to see that we define a method by using the keywords def and end. We named our method initialize and its signature tells us that it accepts six arguments when it is called. The initialize method is important to Ruby. The initialize method is automatically called after a new object is created. Notice that what this method does is initialize six of the object's instance variables to the values passed into the method. Also, remember that Ruby is a dynamic language; thus, variables do not need to be statically typed, and as a result, do not have to be defined before being used. What this means is that we have created a method that initializes the CronJobMgr object's six instance variables and created those variables at the very same time.

Congratulations, you now have a full class with state and actions. However, it still doesn't do very much outside of initializing itself with a few values. Why don't we add one more method and take it out for a spin? Copy the following method into your CronJobMgr class:


def to_s
    "#{@minute}\t#{@hour}\t#{@day}\t#{@month}\t" +
    "#{@weekday}\t#{@command}"
end

What this method does is return a string representing the state of the object as a tabbed list of attributes. In Ruby, the value of the final expression is returned from the method, but if it makes you feel better, you can place the keyword return before the string. The #{. . .} construct just evaluates the expression within the braces; in this case, it returns the value of an instance variable. The plus sign (+) is used to concatenate two strings.

We used double quotes to surround our string rather than single quotes because in Ruby, double quotes are used when we want to use expression evaluations within the string. Otherwise, we can use single quotes to create a literal string. One more thing we want to notice is that to_s is another important Ruby method. All classes in Ruby supply a to_s method that essentially returns a string representation of the object.

Since we now have a class that can possibly do something, let's create a test file and try out our new class. Do so by creating a new file called test.rb and typing in the following code:


require 'CronJobMgr.rb'

cronJobMgr = CronJobMgr.new("50", "11", "*", "*",
     "*", "/usr/bin/date")


puts cronJobMgr

What we have done here is tell the program to use the code in the CronJobMgr.rb by requiring it, and then we created an instance of the CronJobMgr by calling the new method found in all Ruby classes and passing in the arguments that we wish to go to the initialize method. Finally, we printed the state of our object to the screen. Try it out -- call ruby test.rb at the command line and make sure that you see a tabbed list of the attributes you passed into the new method.

Now you know the basics of creating classes, methods, and attributes in Ruby. We need to apply this knowledge to finishing our CronJob class and developing our CronJobMgr class. For these tasks, we will need to pick up a little more Ruby knowledge. In this next section, we'll learn about a quick way to write accessor methods, how to inherit from another class, what code blocks and iterators are, and how to use Ruby's File class, and as a few other interesting little tidbits. So don't stop now, you're doing great, go grab yourself a cup of coffee or tea and read on.

A Little More Ruby

Take a look at the code for the completed CronJob class. You'll notice in the finished class we have added a second initialize method, a method named commandName, and some weird things at the top named attr_reader and attr_writer. Let's start with the most obvious of the three: the new initialize method.

This method is just a default initialize that allows us to create a new CronJob instance without passing in any parameters. It basically sets the state of the object to the default state. The next thing you'll notice is a method named commandName. In this method, we've used the split method provided by the string class to parse out the name of the command we've chosen for the object and return it as a string. This method will be used later in the second article to display the cron job's data in the GUI.

The final additions to the class are the attr_reader and attr_writer methods (yes, these are methods, also). These methods are just shortcuts provided by Ruby for creating reader and writer methods for a class. The only difference between these and normal accessor methods are that they are used just as if they were regular instance variables. If you've done any C# programming, you can think of them as being similar to properties. One final thing to notice in the CronJob class is that the arguments passed into the methods have a colon (:) in front of them. That means that your passing in a symbol, or instead of passing in the value of the variable, you're actually passing in the name of the variable.

We've finally finished our CronJob class. Now we need to create a class whose instance will hold several instances of CronJobs. This class needs to also provide a way to load/save CronJob objects from/to a text file. For these tasks, we will learn how to use inheritance to extend the built-in Array class and we'll also learn how to do text I/O using Ruby's File class. First things first; let's create a class skeleton and add it our CronJobMgr.rb file.

class CronJobMgr < Array
	def loadCronJobs(filename)
	end
	
	def saveCronJobs(filename)
	end
end

In the code above we have created a class that defines two new methods, named loadCronJobs and saveCronJobs. Both of these take an argument named filename. You should also notice that our class declaration is followed by a less-than sign (<) and the class name Array. This is how we show inheritance in Ruby. In essence, we are saying that the class CronJobMgr is a child of Ruby's Array class. Essentially, we have created an array of our own and added two methods to it. Next, we need to flesh out those methods.

Go ahead and take a look at the loadCronJobs method in the CronJobMgr.rb file. The first thing we need to do in our loadCronJobs method is open the file from which we are to extract the jobs. We do this by calling the open method of the File class. The open method uses another nice feature of Ruby known as code blocks.

A code block is just what it suggests, a block of code. The strength of these blocks comes in using them in conjunction with methods, as we see the open method doing here. The open method allows the user to pass in the block of code they wish to have executed. The open method then invokes the block of code and passes to it the argument file. When the block is finished executing, the file is automatically closed.

You'll notice that the rest of the code is littered with code blocks and the methods that use them. Code blocks are a very important feature of Ruby. Combined with methods, code blocks provide a way for users to create iterators. Iterators are basically methods that invoke a block of code repeatedly. Doesn't sound like much, but believe me, start using them and you'll love them. The each_line and upto methods (and the each method in the saveCronJobs method) are examples of iterators in use. The each_line method simply executes the block of code passed into it for each of the lines in the file. The upto and each methods are just as obvious, and I leave it up to you to look into what each does on your own.

Now that you understand the concepts of blocks and iterators, it should be easy to understand the rest of the code. The loadCronJobs method just opens a file and parses each line in the file into the set of attributes that are used to initialize a new CronJob object, which is then pushed into the CronJobMgr object (remember, it is essentially an array with a couple of extra methods).

The saveCronJobs method is even easier to understand. This method opens a file for writing and loops through each CronJob in itself and writes each to the file. The only surprise left is the call to the system method before the close of the saveCronJobs method. This method just allows the user to make calls to commands provided by the operating system.

In this instance, we use the system method to call the crontab command on the new file we have just saved to add all of the jobs to the cron daemon's schedule. Did you get all that? If not, don't worry; you just need to look up a few methods to find out exactly what they do. You can find each of these methods in the built-in classes and methods section of the book Programming Ruby: The Pragmatic Programmer's Guide (a.k.a. the "Pickaxe book").

Final Thoughts

Well, you're done for the moment, barring a few comments to make the code more maintainable. You've learned quite a bit about Ruby in just a short amount of time. I hope I've given you a good overview of the language and went just deeply enough to allow you start coding with Ruby while also leaving a spark of interest in you to make you want to continue learning more.

You now have a good back end to use in our next article, where we will create the GUI front end for the CronJobMgr class using the Tk toolkit. In the interim, if the desire strikes you, try creating a command line interface with Ruby that uses the CronJobMgr class to automate the crontab command. Until the next lesson, enjoy playing around with Ruby.

Christopher Roach recently graduated with a master's in computer science and currently works in Florida as a software engineer at a government communications corporation.


Return to the Mac DevCenter