Comment on this RCR (edit wiki page) | RCRchive home

RCR 211: Normalize filenames before requiring them

submitted by Paul Brannan on Mon Feb 09 2004 10:29:05 AM -0800

Status: pending


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"
  b.rb:
  require 'a'
  puts "b"
  test.rb:
  require './a'
  require './b'

This will print:

  a
  a
  b
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

Vote for this RCR

Strongly opposed [0]
Opposed [0]
Neutral [0]
In favor [4]
Strongly advocate [5]

Change the status of this RCR to:

accepted

rejected

withdrawn


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 <filename>.

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.

Add comments here

Back to RCRchive.


RCR Submission page and RCRchive powered by Ruby, Apache, RuWiki (modified), and RubLog