Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 944
James A. Rosen Avatar asked Dec 03 '22 08:12

James A. Rosen


2 Answers

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
like image 120
kch Avatar answered Dec 23 '22 09:12

kch


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.

like image 28
rampion Avatar answered Dec 23 '22 10:12

rampion