views:

978

answers:

5

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.

+1  A: 

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
kch
how would you pass parameters? Stick 'em on the front of ARGV, perhaps? And how would I pass a block? Or would I have to pass an instance that was guaranteed to respond_to?(:run) (or some other method I pick)?
James A. Rosen
by "parameters" I meant things like the name or instance of the class I want to run.
James A. Rosen
I'm suggesting that you eval the code you want inlined, so, put the scriptize definition in a file and eval it as exemplified.
kch
but how does scriptize know what to do when __FILE__ is the file it's included from? That is, how do I pass in the class/instance/method/block to run when the if clause runs?
James A. Rosen
A: 
load 'somefile'
Paul Betts
Nope, evaluating __FILE__ inside a loaded file has the problem mentioned in the question: it eval's to "./somefile.rb"
James A. Rosen
+1  A: 

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.

kch
I was _just_ typing this up as my own solution. I don't love it, but it seems that I have to pass _something_ to scriptize (either __FILE__ or the name of the class to run), so __FILE__ is just as good/bad as any.
James A. Rosen
though, really, this isn't any shorter than "Foo.new.run(ARGV) if __FILE__ == $0" so I'm not actually buying myself much. It does answer the question about as well as one can, though.
James A. Rosen
Well, check back in 15min, I'll have a fun solution.
kch
Oh well, I'm chickening out actually. I don't think you can go much shorter without secretly hacking around a lot. the `… if __FILE__ == $0` solution is fairly ok and doesn't need any extra code hidden away.
kch
+3  A: 

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.

rampion
Creative, thorough, describes pros and cons. I wish I could give +2.
James A. Rosen
+1  A: 

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
rampion
This is a _really_ good example. Now I'm determined to go dig and see how they do this.
James A. Rosen
It's at the end of 'test/unit.rb'.
rampion