Ruby templates: How to pass variables into inlined

2019-01-10 06:39发布

I have an ERB template inlined into Ruby code:

require 'erb'

DATA = {
    :a => "HELLO",
    :b => "WORLD",
}

template = ERB.new <<-EOF
    current key is: <%= current %>
    current value is: <%= DATA[current] %>
EOF

DATA.keys.each do |current|
    result = template.result
    outputFile = File.new(current.to_s,File::CREAT|File::TRUNC|File::RDWR)
    outputFile.write(result)
    outputFile.close
end

I can't pass the variable "current" into the template.

The error is:

(erb):1: undefined local variable or method `current' for main:Object (NameError)

How do I fix this?

9条回答
小情绪 Triste *
2楼-- · 2019-01-10 07:18

As others said, to evaluate ERB with some set of variables, you need a proper binding. There are some solutions with defining classes and methods but I think simplest and giving most control and safest is to generate a clean binding and use it to parse the ERB. Here's my take on it (ruby 2.2.x):

module B
  def self.clean_binding
    binding
  end

  def self.binding_from_hash(**vars)
    b = self.clean_binding
    vars.each do |k, v|
      b.local_variable_set k.to_sym, v
    end
    return b
  end
end
my_nice_binding = B.binding_from_hash(a: 5, **other_opts)
result = ERB.new(template).result(my_nice_binding)

I think with eval and without ** same can be made working with older ruby than 2.1

查看更多
家丑人穷心不美
3楼-- · 2019-01-10 07:19

For a simple solution, use OpenStruct:

require 'erb'
require 'ostruct'
namespace = OpenStruct.new(name: 'Joan', last: 'Maragall')
template = 'Name: <%= name %> <%= last %>'
result = ERB.new(template).result(namespace.instance_eval { binding })
#=> Name: Joan Maragall

The code above is simple enough but has (at least) two problems: 1) Since it relies on OpenStruct, an access to a non-existing variable returns nil while you'd probably prefer that it failed noisily. 2) binding is called within a block, that's it, in a closure, so it includes all the local variables in the scope (in fact, these variables will shadow the attributes of the struct!).

So here is another solution, more verbose but without any of these problems:

class Namespace
  def initialize(hash)
    hash.each do |key, value|
      singleton_class.send(:define_method, key) { value }
    end 
  end

  def get_binding
    binding
  end
end

template = 'Name: <%= name %> <%= last %>'
ns = Namespace.new(name: 'Joan', last: 'Maragall')
ERB.new(template).result(ns.get_binding)
#=> Name: Joan Maragall

Of course, if you are going to use this often, make sure you create a String#erb extension that allows you to write something like "x=<%= x %>, y=<%= y %>".erb(x: 1, y: 2).

查看更多
一夜七次
4楼-- · 2019-01-10 07:21

In the code from original question, just replace

result = template.result

with

result = template.result(binding)

That will use the each block's context rather than the top-level context.

(Just extracted the comment by @sciurus as answer because it's the shortest and most correct one.)

查看更多
看我几分像从前
5楼-- · 2019-01-10 07:22

EDIT: This is a dirty workaround. Please see my other answer.

It's totally strange, but adding

current = ""

before the "for-each" loop fixes the problem.

God bless scripting languages and their "language features"...

查看更多
神经病院院长
6楼-- · 2019-01-10 07:30

Simple solution using Binding:

b = binding
b.local_variable_set(:a, 'a')
b.local_variable_set(:b, 'b')
ERB.new(template).result(b)
查看更多
小情绪 Triste *
7楼-- · 2019-01-10 07:33

Got it!

I create a bindings class

class BindMe
    def initialize(key,val)
        @key=key
        @val=val
    end
    def get_binding
        return binding()
    end
end

and pass an instance to ERB

dataHash.keys.each do |current|
    key = current.to_s
    val = dataHash[key]

    # here, I pass the bindings instance to ERB
    bindMe = BindMe.new(key,val)

    result = template.result(bindMe.get_binding)

    # unnecessary code goes here
end

The .erb template file looks like this:

Key: <%= @key %>
查看更多
登录 后发表回答