175 Where the Required Things Are

Description

Occasionally, I’ve taken a look at the source for some Ruby module, often because there is no manual or man page, or what documentation is available is outdated or incomplete. Or sometimes I just want to see how some Ruby stuff is implemented.

One such example was from the previous quiz: I want to learn more about the Sys::Uptime module. I have it installed, and the call to require 'sys/uptime' works, but I don’t know how to use it. But, alas, I also don’t know where the installed files are located. The shell command which doesn’t help here, since the module is unlikely to be in the shell’s executable path.

What I would like is a script that works like which but for Ruby modules. Examples:

> ruby modwhich.rb "sys/uptime"
require 'sys/uptime' => /opt/local/lib/ruby/vendor_ruby/1.8/i686-darwin8.11.1/sys/uptime.bundle

> ruby modwhich.rb date
require 'date' => /opt/local/lib/ruby/1.8/date.rb

For extra credit, preserve this behavior when modwhich.rb is the main program, but slightly different behavior if modwhich.rb is required by another script:

> ruby -r modwhich upsince.rb

require 'sys/uptime' => /opt/local/lib/ruby/vendor_ruby/1.8/i686-darwin8.11.1/sys/uptime.bundle
require 'date' => /opt/local/lib/ruby/1.8/date.rb
Last reboot: 2008 Aug 22 at 18:49

Note that we allow upsince.rb to run as normal; the output of modwhich.rb is mixed into stdout.

Summary

Great job on this quiz, everyone who submitted. I know I’ll be adding at least one of these submissions to my toolbox.

Ten solutions were provided this week. I downloaded them all and tested each one in the manner requested in the extra credit. (For anyone who did not attempt the extra credit, make sure to look at the other solutions; adding this capability is only a small amount of extra code.) To test, I used the sample file provided by Michal Suchanek, shown here.

require 'date'
require 'readline'
require 'matrix'
require 'rubygems'
require 'time'
require 'hpricot'
require 'rubyforge'
require 'hoe'
require 'curses' 

I made all modules were installed before testing. (Actually, some tests were done with missing modules, with mostly expected results. In the end, it was more interesting to see which solutions worked with a set of requires that did exist.) A more extensive set of tests would be appropriate to bullet-proof any solutions, but this is sufficient for this quiz.

Ten solutions were provided from nine users (Suchanek providing two solutions). Of those ten, three failed with errors (Shelly, Stevens, Suchanek #2). Two solutions stumbled a bit when rubygems got involved. The first (Bilkovich) failed to find any of the gems: hpricot, rubyforge and hoe. The second (Reitinger) found rubygems but failed on the next (non-gem) require, time; since rubygems does its own thing to require, this is a possible source of problems to be addressed.

It’s possible these problems might be caused by platform differences; I am running ruby 1.8.6 (MacPorts install) on Mac OS X 10.4.11. I didn’t have time to dig into these errors, but if you care to know the specific error, let me know.

The next problem I found was that three solutions (Morin, Phillips, Wille) did not report the locations of the readline and etc modules. The likely cause here is, again, platform differences. These two modules in Mac OS X are “.bundle” files. One solution (Wille) reported the modules as not found, while the other two (Morin, Phillips) simply reported nothing for those two modules.

Interestingly, the “cheat” provided by Wille, gem which, was also unable to locate readline and etc.

That leaves us with two “winners” for this week: Jesse Merriman and Michal Suchanek (his first solution). Both solutions found all the required modules, recursively, including the .bundle files, and didn’t get confused by rubygems. I’m going to look here at Jesse’s solution.

module Kernel
  alias :require_orig :require

  def require mod
    if success = require_orig(mod)
      file = $".last

      $:.each do |dir|
        path = "#{dir}/#{file}"
        if File.readable?(path)
          puts "require #{mod} => #{path}"
          break
        end
      end
    end

    success
  end
end

require is a Kernel method, so the first thing Jesse does is open the Kernel module and keep track of the original require method via an alias. Next, the new require method is defined, and the first thing it does is to call the original method, which is why the alias was needed.

There are three possible results. First, if require_orig throws an exception… well, nothing gets done here. The exception throws out of this method, reproducing the standard behavior. Second, if require_orig returns false (and sets success accordingly), the the body is skipped and false is returned. Again, standard behavior has been maintained. The good stuff is what happens when require_orig returns true.

Jesse makes use of two predefined global variables. $" is an array containing module names loaded by require; calling last on that array gets the most recently loaded module. $: is also an array, but contains the search paths for modules.

So once a file is successfully loaded via a call to require_orig, Jesse checks through the module search paths, and builds a full path for the loaded module, checking that path against File.readable?. If so, the information is printed to standard output and the loop ends.

Finally, most of you should recognize the standard technique for knowing when a script is run rather than loaded via require:

if __FILE__ == $0
  ARGV.each { |mod| require mod }
end 

Make sure to look at Michal Suchanek’s solution as well. The use of module rbconfig and method binding were new to me.

One final note: my machine is running Mac OS X, so of course, all my testing has been using that. This has two implications: some programs may not have worked (pehaps some that I mentioned above), and also that the two “complete” solutions (Merriman, Suchanek) could quite possibly be broken on other platforms. I don’t know. (I’m a bit occupied this week.:) A 100% solid solution should account for all these things.


Wednesday, February 04, 2009