tags:

views:

152

answers:

2

I'm doing some trickery with a bunch of Rake tasks for a complex project, gradually refactoring away some of the complexity in chunks at a time. This has exposed the bizarre web of dependencies left behind by the previous project maintainer.

What I'd like to be able to do is to add a specific path in the project to require's list of paths to be searched, aka $:. However, I only want that path to be searched in the context of one particular method. Right now I'm doing something like this:

def foo()
  # Look up old paths, add new special path.
  paths = $:
  $: << special_path

  # Do work ...
  bar()
  baz()
  quux()

  # Reset.
  $:.clear
  $: << paths
end

def bar()
  require '...' # If called from within foo(), will also search special_path.
  ...
end

This is clearly a monstrous hack. Is there a better way?

+3  A: 

Since $: is an Array, you have to be careful about what you are doing. You need to take a copy (via dup) and replace it later. It' simpler to simply remove what you have added, though:

def foo
  $: << special_path

  # Do work ...
  bar()

ensure
  # Reset.
  $:.delete(special_path)
end

Without more info, it's difficult to know if there is a better way.

Marc-André Lafortune
That seems marginally more sensible, but I was hoping there'd be a sort of "scoped" version of `$:`. Failing that, it would also be good if you can somehow ask `require` to search a specific list of paths (that is, a list which you specify, rather than taking it from `$:`). Nonetheless, +1!
John Feminella
@John, you can easily make your own scoped version of $: (although it won't be lexically scoped: If a function inside the "scoped $:" makes a call outside that scope, the modified $: will be in effect. Just use "begin; $: << special_path; yield; ensure $:.delete(special_path) end". Turn that into a method, toss it into a Module, let it rest for five minutes and then serve with a nice red wine.
Wayne Conrad
Corner case: `$:` _already_ contains `special_path` (`#delete` will remove all occurrences of `special_path`).
Martin Carpenter
A: 

require is actually a method, it's Kernel#require (which calls rb_require_safe) so you could at least perform your hackery in a monkey-patched version. If you like that kind of thing.

  • Alias the orignal require out of the way
  • If passed an absolute path, call the original require method
  • Else iterate over load path by creating an absolute path and calling the original require method.

Just for fun I had a quick bash at that, prototype is below. This isn't fully tested, I haven't checked the semantics of rb_require_safe, and you probably would also need to look at #load and #include for completeness -- and this remains a monkey-patch of the Kernel module. It's perhaps not entirely monstrous, but it's certainly a hack. Your call if it's better or worse than messing with the global $: variable.

module Kernel

  alias original_require require

  # Just like standard require but takes an
  # optional second argument (a string or an
  # array of strings) for additional directories
  # to search.
  def require(file, more_dirs=[])
    if file =~ /^\// # absolute path
      original_require(file)
    else
      ($: + [ more_dirs ].flatten).each do |dir|
        path = File.join(dir, file)
        begin
          return original_require(path)
        rescue LoadError
        end
      end
      raise LoadError,
        "no such file to load -- #{file}"
    end
  end

end

Examples:

require 'mymod'
require 'mymod', '/home/me/lib'
require 'mymod', [ '/home/me/lib', '/home/you/lib' ]
Martin Carpenter
I wound up taking an approach similar to this. Despite the extra code, it's a lot better than having to muck with a global variable.
John Feminella