Ruby templates: How to pass variables into inlined ERB?

RubyTemplatesSyntaxErb

Ruby Problem Overview


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?

Ruby Solutions


Solution 1 - Ruby

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).

Solution 2 - Ruby

Simple solution using Binding:

b = binding
b.local_variable_set(:a, 'a')
b.local_variable_set(:b, 'b')
ERB.new(template).result(b)

Solution 3 - Ruby

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 %>

Solution 4 - Ruby

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.)

Solution 5 - Ruby

require 'erb'

class ERBContext
  def initialize(hash)
    hash.each_pair do |key, value|
      instance_variable_set('@' + key.to_s, value)
    end
  end

  def get_binding
    binding
  end
end

class String
  def erb(assigns={})
    ERB.new(self).result(ERBContext.new(assigns).get_binding)
  end
end

REF : http://stoneship.org/essays/erb-and-the-context-object/

Solution 6 - Ruby

I can't give you a very good answer as to why this is happening because I'm not 100% sure how ERB works, but just looking at the ERB RDocs, it says that you need a binding which is "a Binding or Proc object which is used to set the context of code evaluation".

Trying your above code again and just replacing

result = template.result

with

result = template.result(binding)

made it work.

I'm sure/hope someone will jump in here and provide a more detailed explanation of what's going on. Cheers.

EDIT: For some more information on Binding and making all of this a little clearer (at least for me), check out the Binding RDoc.

Solution 7 - Ruby

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

Solution 8 - Ruby

Maybe the cleanest solution would be to pass specific current local variable to erb template instead of passing the entire binding. It's possible with ERB#result_with_hash method (introduced in Ruby 2.5)

DATA.keys.each do |current|
  result = template.result_with_hash(current: current)
...

Solution 9 - Ruby

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"...

Solution 10 - Ruby

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionivan_ivanovich_ivanoffView Question on Stackoverflow
Solution 1 - RubytoklandView Answer on Stackoverflow
Solution 2 - RubyasferView Answer on Stackoverflow
Solution 3 - Rubyivan_ivanovich_ivanoffView Answer on Stackoverflow
Solution 4 - RubygeekQView Answer on Stackoverflow
Solution 5 - Rubyalvin2yeView Answer on Stackoverflow
Solution 6 - RubytheIVView Answer on Stackoverflow
Solution 7 - RubyakostadinovView Answer on Stackoverflow
Solution 8 - RubyAleksander RyhlitskiView Answer on Stackoverflow
Solution 9 - Rubyivan_ivanovich_ivanoffView Answer on Stackoverflow
Solution 10 - RubyAlex LevineView Answer on Stackoverflow