J
Joel VanderWerf
This is a silly little bit of code, but it's sort of an answer to
http://ruby-talk.org/99775, "RCR: Unique Object for Each source file".
Also, I had been thinking about proposing an RCR to let Kernel#load
return the last value in the file it loads, so it would be easy to
construct an object in a script file and hand it back to the main
program. But after reading back to the thread at
http://ruby-talk.org/62660, that didn't seem like the best way to go.
So, based on Nobu's suggestion in http://ruby-talk.org/62727, here is a
Script class which does the following:
- program.rb:
require 'script'
script = Script.load("script.rb")
p script::VALUE
script.run
- script.rb:
VALUE = [1,2,3]
def run
puts "#{self} running."
end
- output:
$ ruby program.rb
[1, 2, 3]
#<Script:/tmp/script.rb> running.
The value returned by Script.load is a module, and you can access its
constants and module methods, as expected. You can, of course, define
any number of constants or methods in the scope of the script file.
Defining things at the top level in your script file behaves as you
expect, like definitions at the top level of ruby itself, but in the
scope of the script. More precisely, every "def foo" results in
'module_function "foo"' getting invoked, so you can call foo on the
script object itself, back in the main program. This also means that
instance variables defined at the top level of your script are
accessible from the module functions that you define at the top level.
Script has implementations of #load and #require that, called from
within a "script", try to find files locally (relative to the script
file) and then fall back to the Kernel versions of #load and #require,
which look in $LOAD_PATH. If a local file is found, its definitions go
into the same scope as the original script. The intent is that if you
have main script file that loads some others, and everything works
nicely with "ruby script.rb", then it should also work with
Script.load("script.rb"), except that all of your definitions will be
wrapped in the module returned by Script.load. (And it will not break if
you Dir.chdir.)
Also, it is possible to pass "arguments" to a script by attaching a
block to Script.load and using it to define constants in the script's scope.
The implementation of Script is very simple and is included below. A
complete package with documentation and examples is at
http://redshift.sourceforge.net/script.
It's seems like too trivial a thing to put on RAA, especially with a
grand name like "script", so I'll just leave where it is for now, unless
someone has a better suggestion.
-- script.rb
class Script < Module
attr_reader :main_file
attr_reader :dir
attr_reader :loaded_features
class << self
alias load new
end
def initialize(main_file)
@main_file = File.expand_path(main_file)
@dir = File.dirname(@main_file)
@loaded_features = {}
yield self if block_given?
load_in_module(main_file)
end
def load(file, wrap = false)
load_in_module(File.join(@dir, file))
true
rescue MissingFile
super
end
def require(feature)
unless @loaded_features[feature]
@loaded_features[feature] = true
file = File.join(@dir, feature)
file += ".rb" unless /\.rb$/ =~ file
load_in_module(file)
end
rescue MissingFile
@loaded_features[feature] = false
super
end
class MissingFile < LoadError; end
def load_in_module(file)
module_eval(IO.read(file), file)
rescue Errno::ENOENT => e
if /#{file}$/ =~ e.message
raise MissingFile, e.message
else
raise
end
end
def method_added(name)
module_function(name)
end
def to_s
"#<#{self.class}:#{File.join(dir, File.basename(main_file))}>"
end
end
http://ruby-talk.org/99775, "RCR: Unique Object for Each source file".
Also, I had been thinking about proposing an RCR to let Kernel#load
return the last value in the file it loads, so it would be easy to
construct an object in a script file and hand it back to the main
program. But after reading back to the thread at
http://ruby-talk.org/62660, that didn't seem like the best way to go.
So, based on Nobu's suggestion in http://ruby-talk.org/62727, here is a
Script class which does the following:
- program.rb:
require 'script'
script = Script.load("script.rb")
p script::VALUE
script.run
- script.rb:
VALUE = [1,2,3]
def run
puts "#{self} running."
end
- output:
$ ruby program.rb
[1, 2, 3]
#<Script:/tmp/script.rb> running.
The value returned by Script.load is a module, and you can access its
constants and module methods, as expected. You can, of course, define
any number of constants or methods in the scope of the script file.
Defining things at the top level in your script file behaves as you
expect, like definitions at the top level of ruby itself, but in the
scope of the script. More precisely, every "def foo" results in
'module_function "foo"' getting invoked, so you can call foo on the
script object itself, back in the main program. This also means that
instance variables defined at the top level of your script are
accessible from the module functions that you define at the top level.
Script has implementations of #load and #require that, called from
within a "script", try to find files locally (relative to the script
file) and then fall back to the Kernel versions of #load and #require,
which look in $LOAD_PATH. If a local file is found, its definitions go
into the same scope as the original script. The intent is that if you
have main script file that loads some others, and everything works
nicely with "ruby script.rb", then it should also work with
Script.load("script.rb"), except that all of your definitions will be
wrapped in the module returned by Script.load. (And it will not break if
you Dir.chdir.)
Also, it is possible to pass "arguments" to a script by attaching a
block to Script.load and using it to define constants in the script's scope.
The implementation of Script is very simple and is included below. A
complete package with documentation and examples is at
http://redshift.sourceforge.net/script.
It's seems like too trivial a thing to put on RAA, especially with a
grand name like "script", so I'll just leave where it is for now, unless
someone has a better suggestion.
-- script.rb
class Script < Module
attr_reader :main_file
attr_reader :dir
attr_reader :loaded_features
class << self
alias load new
end
def initialize(main_file)
@main_file = File.expand_path(main_file)
@dir = File.dirname(@main_file)
@loaded_features = {}
yield self if block_given?
load_in_module(main_file)
end
def load(file, wrap = false)
load_in_module(File.join(@dir, file))
true
rescue MissingFile
super
end
def require(feature)
unless @loaded_features[feature]
@loaded_features[feature] = true
file = File.join(@dir, feature)
file += ".rb" unless /\.rb$/ =~ file
load_in_module(file)
end
rescue MissingFile
@loaded_features[feature] = false
super
end
class MissingFile < LoadError; end
def load_in_module(file)
module_eval(IO.read(file), file)
rescue Errno::ENOENT => e
if /#{file}$/ =~ e.message
raise MissingFile, e.message
else
raise
end
end
def method_added(name)
module_function(name)
end
def to_s
"#<#{self.class}:#{File.join(dir, File.basename(main_file))}>"
end
end