15 Volts
15 Volts

Reputation: 2077

Is there a way to access a local variable defined inside a block outside the block?

I have written a simple code that evaluates a piece of code and writes the output to a file. In that way it reduces some of my because I need lots and lots of files with return values inside them for every line!.

Anyways, the code I am working with is:

#!/usr/bin/ruby -w

def create(file, code)
    f = code.strip.each_line.map { |cd| cd.strip.then { |c| [c, "# => #{binding.eval(c)}"] } }
    max_length = f.map { |x| x[0].length }.max + 4
    f.map { |v| v[0].ljust(max_length) << v[1] }.join("\n").tap { |data| File.write(file, data + "\n") }
end

puts create(
    File.join(__dir__, 'p.rb'),
    <<~'EOF'
        foo = 1
        bar = 2
        baz, qux = 5, 3
    EOF
)

In this case, the file p.rb is written. The content of p.rb is:

foo = 1            # => 1
bar = 2            # => 2
baz, qux = 5, 3    # => [5, 3]

But the problem occurs when I want the value of a variable. For example:

puts create(
    File.join(__dir__, 'p.rb'),
    <<~'EOF'
        baz, qux = 5, 3
        [baz, qux]
    EOF
)

Output:

/tmp/aa.rb:4:in `block (2 levels) in create': undefined local variable or method `baz' for main:Object (NameError)
    from /tmp/aa.rb:4:in `eval'
    from /tmp/aa.rb:4:in `block (2 levels) in create'
    from /tmp/aa.rb:4:in `then'
    from /tmp/aa.rb:4:in `block in create'
    from /tmp/aa.rb:4:in `each_line'
    from /tmp/aa.rb:4:in `each'
    from /tmp/aa.rb:4:in `map'
    from /tmp/aa.rb:4:in `create'
    from /tmp/aa.rb:9:in `<main>'

Previously I worked in some graphical games that also does this kind of thing after reading a configuration file, but there, I used to define the variables as either global variables (just append $ before variable declaration) or just use instance variables on top self object.

But is there a way to get around the problem I am currently facing? Can I define the variables in binding or some hacks like this?

Upvotes: 1

Views: 163

Answers (1)

Stefan
Stefan

Reputation: 114178

binding returns a new instance every time you call it. You have to send eval to the same binding in order to access local variables that you've created earlier:

def create(code, b = binding)
  width = code.each_line.map(&:length).max
  code.each_line.map do |line|
    '%-*s   #=> %s' % [width, line.chomp, b.eval(line)]
  end
end

puts create <<~'RUBY'
  baz, qux = 5, 3
  baz
  qux
RUBY

Output:

baz, qux = 5, 3    #=> [5, 3]
baz                #=> 5
qux                #=> 3

Note that in the above example, binding will make the method's local variables available to the block:

create 'local_variables'
#=> ["local_variables   #=> [:code, :b, :width]"]

You might want to create a more restricted evaluation context, e.g. (replicating Ruby's main)

def empty_binding
  Object.allocate.instance_eval do
    class << self
      def to_s
        'main'
      end
      alias inspect to_s
    end
    return binding
  end
end

def create(code, b = empty_binding)
  # ...
end

Upvotes: 3

Related Questions