Reputation: 65242
I have a number of Ruby files, each of which declares a Class
, but each of which could conceivably be run from the command line.
I'd like to put the following functionality at the bottom of each file with the least duplication possible:
if __FILE__ == $0
# instantiate the class and pass ARGV to instance.run
end
My first instinct was to do this:
# /lib/scriptize.rb:
Kernel.class_eval do
def scriptize(&block)
block.call(ARGV) if __FILE__ == $0
end
end
# /lib/some_other_file.rb:
include 'scriptize'
class Foo
# ...
end
scriptize { |args| Foo.new.run(args) }
But that doesn't work because __FILE__
is evaluated in scriptize.rb
, so it's never Foo.
I imagine the solution is to literally inline the contents of scriptize.rb
, but I don't know the syntax. I could use eval
, but that's still quite a bit of duplication -- it can't really be reduced to a method I add to Kernel
.
Upvotes: 4
Views: 5734
Reputation: 515
We can use eval(IO.read('filename.rb'), binding)
Example:-
def setup
@driver = Selenium::WebDriver.for :chrome
@base_url = "http://stage.checkinforgood.com/"
@driver.manage.timeouts.implicit_wait = 30
@verification_errors = []
end
def teardown
@driver.quit
assert_equal [], @verification_errors
end
require "selenium-webdriver"
require "test/unit"
class C4g < Test::Unit::TestCase
eval(IO.read('setup.rb'), binding)
def test_login
@driver.get "http://stage.checkinforgood.com/"
@driver.find_element(:link, "Sign In").click
@driver.find_element(:id, "user_email").clear
@driver.find_element(:id, "user_email").send_keys "[email protected]"
@driver.find_element(:id, "user_password").clear
@driver.find_element(:id, "user_password").send_keys "test123"
@driver.find_element(:id, "user_submit").click
end
def element_present?(how, what)
@driver.find_element(how, what)
true
rescue Selenium::WebDriver::Error::NoSuchElementError
false
end
def verify(&blk)
yield
rescue Test::Unit::AssertionFailedError => ex
@verification_errors << ex
end
end
Now we can run,
$ruby c4g.rb
Upvotes: 1
Reputation: 79552
Try evaling it.
eval(IO.read(rubyfile), binding)
That's what Rails' initializer does when loading files in config/environments
, because it needs to evaluate them within the Rails::Initializer.run
block.
binding
is a ruby method that'll return the current context, when passed to eval
, causes it to evaluate the code within the calling environment.
Try this:
# my_class.rb
class MyClass
def run
puts 'hi'
end
end
eval(IO.read('whereami.rb'), binding)
# whereami.rb
puts __FILE__
$ ruby my_class.rb
my_class.rb
Upvotes: 11
Reputation: 89053
Another way to do it is how Test::Unit
does it. A test case file only has a class definition in it (and a require 'test/unit'
).
The 'test/unit' library sets up an at_exit
handler that automatically runs any test cases and suites. If your most common case is going to be running these class files, and occasionally using them as libraries, you could do something similar, and set a global to disable autorun when it was included as a library.
For example:
# tc_mytest.rb
require 'test/unit'
class TC_MyTest < Test::Unit::TestCase
def test_succeed
assert(true, 'Assertion was true.')
end
def test_fail
assert(false, 'Assertion was false.')
end
end
No boilerplater required to run:
% ruby tc_mytest.rb
Loaded suite tc_mytest
Started
F.
Finished in 0.007241 seconds.
1) Failure:
test_fail(TC_MyTest) [tc_mytest.rb:8]:
Assertion was false.
<false> is not true.
2 tests, 2 assertions, 1 failures, 0 errors
Upvotes: 1
Reputation: 89053
Use caller
to determine how close you are to the top of the call stack:
---------------------------------------------------------- Kernel#caller
caller(start=1) => array
------------------------------------------------------------------------
Returns the current execution stack---an array containing strings
in the form ``_file:line_'' or ``_file:line: in `method'_''. The
optional _start_ parameter determines the number of initial stack
entries to omit from the result.
def a(skip)
caller(skip)
end
def b(skip)
a(skip)
end
def c(skip)
b(skip)
end
c(0) #=> ["prog:2:in `a'", "prog:5:in `b'", "prog:8:in `c'", "prog:10"]
c(1) #=> ["prog:5:in `b'", "prog:8:in `c'", "prog:11"]
c(2) #=> ["prog:8:in `c'", "prog:12"]
c(3) #=> ["prog:13"]
This gives this definition for scriptize
# scriptize.rb
def scriptize
yield ARGV if caller.size == 1
end
Now, as an example, we can use two libraries/executables that require each other
# libexA.rb
require 'scriptize'
require 'libexB'
puts "in A, caller = #{caller.inspect}"
if __FILE__ == $0
puts "A is the main script file"
end
scriptize { |args| puts "A was called with #{args.inspect}" }
# libexB.rb
require 'scriptize'
require 'libexA'
puts "in B, caller = #{caller.inspect}"
if __FILE__ == $0
puts "B is the main script file"
end
scriptize { |args| puts "B was called with #{args.inspect}" }
So when we run from the command line:
% ruby libexA.rb 1 2 3 4
in A, caller = ["./libexB.rb:2:in `require'", "./libexB.rb:2", "libexA.rb:2:in `require'", "libexA.rb:2"]
in B, caller = ["libexA.rb:2:in `require'", "libexA.rb:2"]
in A, caller = []
A is the main script file
A was called with ["1", "2", "3", "4"]
% ruby libexB.rb 4 3 2 1
in B, caller = ["./libexA.rb:2:in `require'", "./libexA.rb:2", "libexB.rb:2:in `require'", "libexB.rb:2"]
in A, caller = ["libexB.rb:2:in `require'", "libexB.rb:2"]
in B, caller = []
B is the main script file
B was called with ["4", "3", "2", "1"]
So this shows the equivalence of using scriptize and if $0 == __FILE__
However, consider that:
if $0 == __FILE__ ... end
is a standard ruby idiom, easily recognized by others reading your coderequire 'scriptize'; scriptize { |args| ... }
is more typing for the same effect.In order for this to really be worth it, you'd need to have more commonality in the body of scriptize - initializing some files, parsing arguments, etc. Once it gets complex enough, you might be better off with factoring out the changes in a different way - maybe passing scriptize your class, so it can instantiate them and do the main script body, or have a main script that dynamically requires one of your classes depending on what the name is.
Upvotes: 5
Reputation: 79552
Or, you could simply pass __FILE__
to scriptize
# /lib/scriptize.rb:
module Kernel
def scriptize(calling_file, &block)
block.call(ARGV) if calling_file == $0
end
end
# /lib/some_other_file.rb:
...
scriptize(__FILE__) { |args| Foo.new.run(args) }
I also took the time to do away with the class_eval
thing. (and you might also do away with the whole module
thing, since Kernel
is your scope by default.
Upvotes: 1