【问题标题】:How do I include Ruby source inline in another file?如何在另一个文件中包含 Ruby 源代码?
【发布时间】:2009-03-05 00:50:13
【问题描述】:

我有许多 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 的方法。

【问题讨论】:

  • 我不确定我是否看到了对它的任何需求。你能扩展一个为什么文件末尾的单行不会这样做吗?类似Foo.new.run(*ARGV) if __FILE__ == $0
  • 可能有使用Kernel#caller的解决方案。如果您希望我详细说明,请告诉我。

标签: ruby command-line


【解决方案1】:

使用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 是一个标准的 ruby​​ 习语,阅读您的代码的其他人很容易识别
  2. require 'scriptize'; scriptize { |args| ... } 为相同的效果输入更多内容。

为了真正值得这样做,您需要在 scriptize 的主体中具有更多的通用性 - 初始化一些文件、解析参数等。一旦变得足够复杂,您最好还是分解以不同的方式进行更改 - 可能通过脚本化您的类,因此它可以实例化它们并执行主脚本主体,或者拥有一个根据名称动态需要您的类之一的主脚本。

【讨论】:

  • 创意、透彻、描述利弊。我希望我能给 +2。
【解决方案2】:

尝试评估它。

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

【讨论】:

  • 如何传递参数?把它们贴在 ARGV 的前面,也许?我将如何通过一个街区?或者我是否必须传递一个保证响应的实例?(:运行)(或我选择的其他方法)?
  • “参数”是指我想运行的类的名称或实例。
  • 我建议您评估您想要内联的代码,因此,将脚本化定义放在一个文件中并按照示例对其进行评估。
  • 但是当 FILE 是包含它的文件时,scriptize 如何知道要做什么?即if子句运行时如何传入class/instance/method/block来运行?
【解决方案3】:

或者,您可以简单地将__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 默认是你的作用域。

【讨论】:

  • 只是将其作为我自己的解决方案输入。我不喜欢它,但似乎我必须将 something 传递给脚本化(FILE 或要运行的类的名称),所以 FILE 和其他的一样好/坏。
  • 不过,实际上,这并不比 "Foo.new.run(ARGV) if FILE == $0" 短,所以我实际上并没有给自己买太多.不过,它确实可以很好地回答这个问题。
  • 好吧,15 分钟后再回来看看,我会有一个有趣的解决方案。
  • 哦,好吧,我实际上是在退缩。我不认为你可以在不偷偷摸摸的情况下走得更短。 … if __FILE__ == $0 解决方案相当不错,不需要隐藏任何额外的代码。
【解决方案4】:

另一种方法是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

【讨论】:

  • 这是一个真正的好例子。现在我决心去挖掘,看看他们是怎么做到的。
【解决方案5】:

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

例子:-

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

【讨论】:

    【解决方案6】:
    load 'somefile'
    

    【讨论】:

    • 不,在加载的文件中评估 FILE 有问题中提到的问题:它评估到“./somefile.rb”
    猜你喜欢
    • 2014-11-18
    • 2018-03-15
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-06
    • 2020-10-09
    相关资源
    最近更新 更多