Episode #055: Runnable Library

Upgrade to download episode video.

Episode Script

Back in Episode 40, we converted a one-off command-line invocation of Ruby into a reusable script, and then into a method for use in larger programs. Here's what we ended up with:

# accepts an IO object, returns a String
def rfc822_to_json(input)
  headers    = {}
  record_sep = "\r\n"
  input.each_line(record_sep) do |line|
    fields = line.split(/:\s*/)
    break if fields.size < 2
    headers[fields[0]] = fields[1].chomp(record_sep)
  end
  JSON.pretty_generate(headers)
end

It's likely that we may still have the need to run manual command-line conversions of RFC822 to JSON from time to time. As it happens, there is a simple and quick way to use a Ruby file as both a library and a runnable script. But in order to use it, we first need to understand two Ruby features.

The first feature is the __FILE__ constant (for brevity I'm just going to call this the “FILE constant” from here on out). This constant always contains the name of the file it is written in. To demonstrate, we'll create a Ruby file named foo.rb, which outputs the name of this constant.

puts "__FILE__: #{__FILE__}"

When we run this file with ruby, we see that it outputs its own filename.

$ ruby foo.rb 
__FILE__: foo.rb

If we run it again prefixed with ./, we can see that the __FILE__ constant echoes the filename as it was given to the ruby executable.

$ ruby ./foo.rb                                                                                                      
__FILE__: ./foo.rb

The second feature we need to understand is the $PROGRAM_NAME global variable. Let's add a line to our script which outputs the value of this global.

puts "__FILE__: #{__FILE__}"
puts "$PROGRAM_NAME: #{$PROGRAM_NAME}"

The $PROGRAM_NAME variable contains the name of the currently running program, which in this case is the same as the name of the file. We can see this when we run the script again:

$ ruby foo.rb 
__FILE__: foo.rb
$PROGRAM_NAME: foo.rb

You may sometimes see this variable referenced using its shorter alias, $0.

puts "__FILE__: #{__FILE__}"
puts "$PROGRAM_NAME: #{$PROGRAM_NAME}"
puts "$0: #{$0}"
$ ruby foo.rb 
__FILE__: foo.rb
$PROGRAM_NAME: foo.rb
$0: foo.rb

So far, it seems like both the __FILE__ constant and the $PROGRAM_NAME global are identical. But let's see what happens when we require this file as a library from another script. We create a script called client.rb which requires foo.rb, then run client.rb from the command line.

puts "Requiring foo.rb..."
require './foo'
$ ruby client.rb 
Requiring foo.rb...
__FILE__: /home/avdi/Dropbox/rubytapas/054-runnable-library/foo.rb
$PROGRAM_NAME: client.rb
$0: client.rb

We can see some changes. The constant and the variable no longer have the same value. The __FILE__ constant contains the fully-qualified name of the file it was found in, foo.rb. Whereas the $PROGRAM_NAME variable contains the name of the file we passed to ruby to run.

With this knowledge in hand, we have what we need to make an rfc822_to_json library which doubles as a runnable script. All we need to do is put a conditional comparing the $PROGRAM_NAME to the __FILE__ constant at the end of the file. If they match, that means this the file is being run as a script. In that case, we call the rfc822_to_json method with $stdin as its input. Otherwise, in the case the file is being loaded as a library, we do nothing.

if $PROGRAM_NAME == __FILE__
  rfc822_to_json($stdin)
end

If you come from a Python background, this trick is probably familiar to you. In the Python world it's a common technique, both as a way to make a library double as a script, and as a way to embed the tests for a given file directly in the file itself. In the latter case, running the file standalone would run its tests whereas loading it as a library would ignore them.

In my experience it's not nearly as common an idiom in Ruby, which is why I decided to go over it. It isn't every day you need a library to double as a script, but when you do, this trick is quicker than having to write a separate executable script just to load the library and then execute a method.

That's all for now. Happy hacking!