James A. Rosen
James A. Rosen

Reputation: 65242

How do I include Ruby source inline in another file?

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

Answers (6)

Thiyagarajan Veluchamy
Thiyagarajan Veluchamy

Reputation: 515

We can use eval(IO.read('filename.rb'), binding)

Example:-

setup.rb

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

c4g.rb

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

kch
kch

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

rampion
rampion

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

rampion
rampion

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:

  1. if $0 == __FILE__ ... end is a standard ruby idiom, easily recognized by others reading your code
  2. require '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

kch
kch

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

Ana Betts
Ana Betts

Reputation: 74654

load 'somefile'

Upvotes: -1

Related Questions