如何在另一个文件中包含Ruby源内联?

时间:2009-03-05 00:50:13

标签: ruby command-line

我有许多Ruby文件,每个文件声明一个Class,但每个文件都可以从命令行运行。

我想在每个文件的底部放置以下功能,尽可能减少重复:

if __FILE__ == $0
  # instantiate the class and pass ARGV to instance.run
end

我的第一直觉是这样做:

# /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) }

但这不起作用,因为在__FILE__中评估scriptize.rb,所以永远不会 Foo。

我想解决方案是在字面上内联scriptize.rb的内容,但我不知道语法。我可以使用eval,但这仍然有点重复 - 它实际上不能简化为我添加到Kernel的方法。

6 个答案:

答案 0 :(得分:11)

尝试评估它。

eval(IO.read(rubyfile), binding)

这是Rails的初始化程序在config/environments中加载文件时所执行的操作,因为它需要在Rails::Initializer.run块中对它们进行评估。

binding是一个ruby方法,它会在传递给eval时返回当前上下文,使其评估调用环境中的代码。


试试这个:

  # 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

答案 1 :(得分:4)

使用caller确定您与调用堆栈顶部的距离:

---------------------------------------------------------- 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"]

这给出了scriptize

的定义
# scriptize.rb
def scriptize
    yield ARGV if caller.size == 1
end

现在,作为一个例子,我们可以使用两个需要彼此的库/可执行文件

# 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}" }

所以当我们从命令行运行时:

% 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"]

因此,这显示了使用scriptize和if $0 == __FILE__

的等价性

但是,请考虑:

  1. if $0 == __FILE__ ... end是标准的红宝石成语,很容易被其他人阅读您的代码识别
  2. require 'scriptize'; scriptize { |args| ... }为同样的效果打字更多。
  3. 为了让它真的值得,你需要在scriptize的主体中有更多的共性 - 初始化一些文件,解析参数等等。一旦它变得足够复杂,你可能会更好地解决问题以不同的方式进行更改 - 可能会通过脚本化您的类,因此它可以实例化它们并执行主脚本体,或者有一个主脚本根据名称动态地需要您的一个类。

答案 2 :(得分:1)

或者,您只需将__FILE__传递给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) }

我也花时间取消了class_eval的事情。 (您也可以取消整个module内容,因为默认情况下Kernel是您的范围。

答案 3 :(得分:1)

另一种方法是Test::Unit如何做到这一点。测试用例文件中只有一个类定义(和require 'test/unit')。

'test / unit'库设置一个at_exit处理程序,可以自动运行任何测试用例和套件。如果您最常见的情况是运行这些类文件,并偶尔将它们用作库,您可以执行类似的操作,并设置全局以在将其作为库包含时禁用自动运行。

例如:

 # 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

无需运行任何寻找锅炉:

% 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

答案 4 :(得分:1)

我们可以使用eval(IO.read('filename.rb'),绑定)

实施例: -

的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 "vtr@weboniselab.com"
    @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

现在我们可以跑了,

$ ruby​​ c4g.rb

答案 5 :(得分:-1)

load 'somefile'