ruby picture

RCR 211: Normalize filenames before requiring them

Submitted by Paul Brannan (Mon Feb 09 15:02:05 UTC 2004)

Abstract

Filenames should be normalized before the file is required. Otherwise, requiring files by two different names will result in the file being loaded twice.

Problem

Consider the following code:

  a.rb:
  puts "a"</pre>
  b.rb:

    
  require 'a'
  puts "b"</pre>
  test.rb:

    
  require './a'
  require './b'</pre>

This will print:
  a

    
  a
  b</pre>

because a.rb is loaded first by the name 'a.rb' and then by the name './a.rb'. This is probably not what is intended, and the only workarounds are:

The above code is probably not a good idea to begin with (requring files from the current directory is bad, in case the user runs from a different directory than expected), but it is a simple way to demonstrate the problem.

Proposal

The require method should be changed to:
  1. Find the file to require by searching the search path
  2. Expand the pathanme of the file being required by using File#expand_path
  3. Determine if the resulting pathname refers to a symlink; if it does, then read the link and go back to step 2.

Analysis

This should make it possible to require a file by two different names without problems. It also allows the user to require a symlink and the real file, and the file will only be loaded once.

Implementation

Sample Ruby code that does this:

def find_file_in_path(file, path=$:, extensions=['', '.so', '.rb'])
  path.each do |dir|
    pathname = File.join(dir, file)
    extensions.each do |ext|
      filename = pathname + ext
      if File.exists?(filename) then
        return File.expand_path(filename)
      end
    end
  end
  return file
end

def realpath(file)
  if not File.respond_to?(:readlink) then
    return file
  end
  total = ''
  File.expand_path(file).split(File::SEPARATOR).each do |piece|
    next if piece == ""
    if File.symlink?(total + File::SEPARATOR + piece) then
      total += File::SEPARATOR + piece
      begin
        total =  File.expand_path(
            File.readlink(total),
            File.dirname(total))
      end while File.symlink?(total)
    else
      total << File::SEPARATOR + piece
    end
  end
  return total
end

$__old_require__ = method(:require)
def require(file)
  file_to_require = find_file_in_path(file)
  $__old_require__.call(realpath(file_to_require))
end

ruby picture
Comments Current voting

I'd also like to see __FILE__ normalised in this way. While researching an RCR that I'm preparing, I found the following:

I have C:/home/x/x.rb:

  require "y/x.rb"
  puts "In x.rb, __FILE__ = #{FILE}"

I also have C:/home/x/y/x.rb:

  puts "In y/x.rb, __FILE__ = #{FILE}"

If I run from C:/home/x, I get:

  In y/x.rb, __FILE__ = ./y/x.rb 
  In x.rb, __FILE__ = \C:/home/x/x.rb 

Changing the require in C:/home/x/x.rb to "x/y/x.rb" and running from C:/home, I get:

  In y/x.rb, __FILE__ = ./x/y/x.rb 
  In x.rb, __FILE__ = \C:/home/x/x.rb 

Restoring the require to "y/x.rb" and running from C:/home with "ruby -Ic:/home/x" shows:

  In y/x.rb, __FILE__ = x/y/x.rb 
  In x.rb, __FILE__ = x/x.rb 

Same, but from C:\Windows ("ruby -I\home\x \home\x\x.rb"):

  In y/x.rb, __FILE__ = \home\x/y/x.rb 
  In x.rb, __FILE__ = /home/x/x.rb 

This seems a bit ... ugly. I'm not completely sure that __FILE__ itself should be fully resolved, but it seems that there's something odd here.

--Austin


The API for "require" is

  require <feature>

not .

Although the implementation concatenate the feature name and path in $LOAD_PATH variable to find loading library file, it does not stop being feature name. When you require two libraries "a" and "./a", they might be different just because you're requiring two different features.

But I'm going to store full path used to load a library in the $" variable (but no normalization), to implement requiring libraries relative from __FILE__.

-- matz.


Strongly opposed 0
Opposed 0
Neutral 0
In favor 4
Strongly advocate 6
ruby picture
If you have registered at RCRchive, you may now sign in below. If you have not registered, you may sign up for a username and password. Registering enables you to submit new RCRs, and vote and leave comments on existing RCRs.
Your username:
Your password:

ruby picture

Powered by .