Substituting Variables into an Existing String

Problem

You want to create a string that contains Ruby expressions or variable substitutions, without actually performing the substitutions. You plan to substitute values into the string later, possibly multiple times with different values each time.

Solution

There are two good solutions: printf-style strings, and ERB templates.

Ruby supports a printf-style string format like C's and Python's. Put printf directives into a string and it becomes a template. You can interpolate values into it later using the modulus operator:

template = 'Oceania has always been at war with %s.' template % 'Eurasia' # => "Oceania has always been at war with Eurasia." template % 'Eastasia' # => "Oceania has always been at war with Eastasia." 'To 2 decimal places: %.2f' % Math::PI # => "To 2 decimal places: 3.14" 'Zero-padded: %.5d' % Math::PI # => "Zero-padded: 00003"

An ERB template looks something like JSP or PHP code. Most of it is treated as a normal string, but certain control sequences are executed as Ruby code. The control sequence is replaced with either the output of the Ruby code, or the value of its last expression:

require 'erb' template = ERB.new %q{Chunky <%= food %>!} food = "bacon" template.result(binding) # => "Chunky bacon!" food = "peanut butter" template.result(binding) # => "Chunky peanut butter!"

You can omit the call to Kernel#binding if you're not in an irb session:

puts template.result # Chunky peanut butter!

You may recognize this format from the .rhtml files used by Rails views: they use ERB behind the scenes.

Discussion

An ERB template can reference variables like food before they're defined. When you call ERB#result, or ERB#run, the template is executed according to the current values of those variables.

Like JSP and PHP code, ERB templates can contain loops and conditionals. Here's a more sophisticated template:

template = %q{ <% if problems.empty? %> Looks like your code is clean! <% else %> I found the following possible problems with your code: <% problems.each do |problem, line| %> * <%= problem %> on line <%= line %> <% end %> <% end %>}.gsub(/^s+/, '') template = ERB.new(template, nil, '<>') problems = [["Use of is_a? instead of duck typing", 23], ["eval() is usually dangerous", 44]] template.run(binding) # I found the following possible problems with your code: # * Use of is_a? instead of duck typing on line 23 # * eval() is usually dangerous on line 44 problems = [] template.run(binding) # Looks like your code is clean!

ERB is sophisticated, but neither it nor the printf-style strings look like the simple Ruby string substitutions described in Recipe 1.2. There's an alternative. If you use single quotes instead of double quotes to define a string with substitutions, the substitutions won't be activated. You can then use this string as a template with eval:

class String def substitute(binding=TOPLEVEL_BINDING) eval(%{"#{self}"}, binding) end end template = %q{Chunky #{food}!} # => "Chunky #{food}!" food = 'bacon' template.substitute(binding) # => "Chunky bacon!" food = 'peanut butter' template.substitute(binding) # => "Chunky peanut butter!"

You must be very careful when using eval: if you use a variable in the wrong way, you could give an attacker the ability to run arbitrary Ruby code in your eval statement. That won't happen in this example since any possible value of food gets stuck into a string definition before it's interpolated:

food = '#{system("dir")}' puts template.substitute(binding) # Chunky #{system("dir")}!

 

See Also

Категории