Как включить встроенный источник Ruby в другой файл? - PullRequest
4 голосов
/ 05 марта 2009

У меня есть несколько файлов 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 ]

11 голосов
/ 05 марта 2009

Попробуйте оценить его.

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
4 голосов
/ 05 марта 2009

Используйте 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"]

Итак, это показывает эквивалентность использования скриптов и if $0 == __FILE__

Однако учтите, что:

  1. if $0 == __FILE__ ... end - это стандартная рубиновая идиома, которую легко узнают другие, читающие ваш код
  2. require 'scriptize'; scriptize { |args| ... } больше печатать для того же эффекта.

Чтобы это действительно стоило того, вам нужно иметь больше общности в основной части сценариев - инициализации некоторых файлов, разборе аргументов и т. Д. Как только это станет достаточно сложным, вам, возможно, будет лучше с выделением изменения по-другому - возможно, передача скриптов в ваш класс, чтобы он мог создавать их экземпляры и выполнять основное тело скрипта, или иметь основной скрипт, который динамически требует один из ваших классов в зависимости от имени.

1 голос
/ 27 августа 2012

Мы можем использовать 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

1 голос
/ 05 марта 2009

Другой способ сделать это - 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
1 голос
/ 05 марта 2009

Или вы можете просто передать __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 является вашей областью по умолчанию.

0 голосов
/ 05 марта 2009
load 'somefile'
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...