Ruby模板:如何将变量传递到内联ERB中?

57

我有一个内联到Ruby代码中的ERB模板:

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

我无法将变量"current"传递到模板中。

错误信息如下:

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

我该怎么修复这个问题?

10个回答

69

对于一个简单的解决方案,可以使用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
上面的代码很简单,但至少存在两个问题:1)它依赖于OpenStruct,访问不存在的变量会返回nil,而您可能更希望它产生嘈杂错误。2)binding在一个块内被调用,在闭包中,因此它包含作用域中的所有局部变量(实际上,这些变量将掩盖结构的属性!)
所以这是另一种解决方案,更冗长但没有任何这些问题:
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
当然,如果你经常使用这个功能,请确保创建一个String#erb扩展,以便你可以编写像"x=<%= x %>, y=<%= y %>".erb(x: 1, y: 2)这样的内容。

你测试过这个吗?在我的系统上,你的精确代码会产生“NameError: undefined local variable or method `name' for main:Object.”(编辑:似乎是1.9.2的问题https://dev59.com/KXA75IYBdhLWcg3wipmH) - Ryan Tate
@Ryan。确实,我只在1.8.7中进行了测试并更新。我会在你提供的问题中添加一个答案,我认为instance_eval是最简单的解决方案。感谢指出这个问题。 - tokland
namespace.instance_eval { binding } 是什么意思? - Jwan622
我正在阅读instance_eval的作用,但我不知道在namespace上下文中执行绑定是什么意思。从文档中得知:在接收者(obj)的上下文中评估包含Ruby源代码的字符串或给定的块。为了设置上下文,当代码执行时,变量self设置为obj,使代码可以访问obj的实例变量。 - Jwan622

31

使用Binding的简单解决方案:

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

2
local_variable_set 是在 Ruby 2.1 中引入的。 - kbrock

11

明白了!

我创建一个绑定类

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

并且将实例传递给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

这个 .erb 模板文件长这样:

Key: <%= @key %>

9
不必要。在您原始问题的代码中,只需将“result = template.result”替换为“result = template.result(binding)”,这将使用每个块的上下文而不是顶级上下文。 - sciurus

8
在原问题的代码中,只需替换
result = template.result

使用

result = template.result(binding)

那将使用每个块的上下文而不是顶层上下文。
(只提取@sciurus的评论作为答案,因为它是最短和最正确的。)

7
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/ 本文介绍了在Ruby on Rails中使用ERB模板时,如何使用“上下文对象”来传递变量和方法。上下文对象是在渲染模板时自动创建的,其中包含了模板中可见的所有实例变量和已定义的实例方法。因此,可以使用上下文对象来调用实例方法或者访问实例变量而不需要通过参数传递。
从本篇文章中,我们可以学习到如何正确地使用上下文对象来简化代码并提高效率。

4
我不能给你一个非常好的答案,解释为什么会出现这种情况,因为我不是100%确定ERB是如何工作的,但是看一下ERB RDocs,它说你需要一个binding,它是“用于设置代码评估上下文的Binding或Proc对象”。尝试使用你上面的代码,只需替换
result = template.result

使用

result = template.result(binding)

让它正常工作。

我相信/希望有人会跳进来,提供更详细的解释。谢谢。

编辑:为了获得关于 Binding 的更多信息,并使所有这些变得更加清晰(至少对我来说是这样),请查看 绑定 RDoc


2
也许最干净的解决方案是将特定的“current”本地变量传递给erb模板,而不是传递整个“binding”。这是使用ERB#result_with_hash方法(在Ruby 2.5中引入)可能的。
DATA.keys.each do |current|
  result = template.result_with_hash(current: current)
...

1

正如其他人所说,要使用一组变量评估ERB,您需要一个合适的绑定。有一些解决方案是定义类和方法,但我认为最简单、最具控制性和安全性的方法是生成一个干净的绑定并将其用于解析ERB。这是我的做法(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)

我认为使用eval并且不使用**,可以使得旧版本的Ruby(低于2.1)也能正常工作。


0

编辑:这是一个不太正规的解决方法。请查看我的另一个答案。

这很奇怪,但是添加

current = ""

在“for-each”循环之前修复问题。

上帝保佑脚本语言及其“语言特性”……


我认为这是因为在 Ruby 1.8 中,块参数不是真正的绑定变量。这在 Ruby 1.9 中已经改变了。 - Vincent Robert
1
ERB 使用默认绑定来评估变量的是顶层绑定。除非您先在那里使用它(分配一个值给它),否则您的变量 "current" 在顶层绑定中不存在。 - molf


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接