#puts inside ERB

(Gavin Kistner) #1

I see this has been suggested before, in [ruby-talk:126986] and [ruby-talk:127097], but with no followup reports of success or failure.

How would I patch ERB so that puts and print were aliased to concatenate onto the current eoutvar? This aliasing should be setup at the beginning of #result, and removed at the end of #result.

Has anyone tried this already? Is there any compelling reason why this shouldn't be done? (I know I was bitten by this a lot as an ERB newb, and I've seen many others with similar complaints.) For the rare case where stdout is needed (or any other IO) an explicit call like $stdout.puts can be used instead.

(Gavin Kistner) #2

Here's my horrific hack that does it. It's particularly hacky because it requires that the 'eoutvar' be set to have a @ at the beginning of the name (an instance variable). The real solution I suppose requires a change to the ERB::Compiler#compile method, to replace calls to puts and print with the local variable concatenation inline. But that was beyond my foo.

#in the ERB class
     def initialize(str, safe_level=nil, trim_mode=nil, eoutvar='_erbout')
         @safe_level = safe_level
         compiler = ERB::Compiler.new(trim_mode)
         set_eoutvar(compiler, eoutvar)
         @src = <<-ENDIOBORK
             module Kernel
                 alias_method :_oldputs, :puts
                 alias_method :_oldprint, :print
                 def puts(s)
                     #{eoutvar}<<s+"\\n"
                 end
                 def print(s)
                     #{eoutvar}<<s
                 end
             end
         ENDIOBORK
         @src << compiler.compile(str)
         @src << "\n" + <<-FIXIOBORK
             module Kernel
                 alias_method :puts, :_oldputs
                 alias_method :print, :_oldprint
             end
             #{eoutvar}
         FIXIOBORK
         @filename = nil
     end

···

On Aug 18, 2005, at 8:47 AM, Gavin Kistner wrote:

How would I patch ERB so that puts and print were aliased to concatenate onto the current eoutvar? This aliasing should be setup at the beginning of #result, and removed at the end of #result.

(Lyndon Samson) #3

Maybe replace _erbout with an IO object ( including StringIO? )

then you could have explicit _erbout.puts calls

(Mark Hubbart) #4

<snip>

How about this? Still somewhat hacky, but maybe less breakable. It
should work unless you supply a binding in the Kernel module context;
the new #puts is defined in Object.

···

On 8/18/05, Gavin Kistner <gavin@refinery.com> wrote:

On Aug 18, 2005, at 8:47 AM, Gavin Kistner wrote:
> How would I patch ERB so that puts and print were aliased to
> concatenate onto the current eoutvar? This aliasing should be setup
> at the beginning of #result, and removed at the end of #result.

Here's my horrific hack that does it. It's particularly hacky because
it requires that the 'eoutvar' be set to have a @ at the beginning of
the name (an instance variable). The real solution I suppose requires
a change to the ERB::Compiler#compile method, to replace calls to
puts and print with the local variable concatenation inline. But that
was beyond my foo.

-----

require 'erb'

class Object
  # Object's puts overrides Kernel's.
  # Use a thread-local default output if available.
  def puts(*args)
    if Thread.current[:default_output]
      Thread.current[:default_output].puts(*args)
    else
      super
    end
  end
  # Probably should do the same with Kernel#print, #p, etc.
end

# This module turns concatenationability (<<) into writability.
# We'll use it on the eoutvar to let it be used as an IO, too.
module Writeable
  def write(other)
    self << other
    other.size
  end
  def puts(*other)
    other.each{|o| self << (o[-1] == ?\n ? o : o + "\n") }
    nil
  end
  def print(other)
    self << other
    nil
  end
end

class ERB
  def set_eoutvar(compiler, eoutvar = '_erbout')
    compiler.put_cmd = "#{eoutvar}.concat"

    cmd = []
    cmd.push "#{eoutvar} = ''"
    cmd.push "#{eoutvar}.extend Writeable"
    cmd.push "Thread.current[:default_output] = #{eoutvar}"
    
    compiler.pre_cmd = cmd

    cmd = []
    cmd.push "Thread.current[:default_output] = nil"
    cmd.push(eoutvar)

    compiler.post_cmd = cmd
  end
end

(Gavin Kistner) #5

It already works to have explcit _erbout.concat or _erbout.<< calls; the desire here is not one of functionality, but instead of having the raw print and puts methods work as many people expect them to inside an ERB template.

···

On Aug 19, 2005, at 4:03 AM, Lyndon Samson wrote:

Maybe replace _erbout with an IO object ( including StringIO? )

then you could have explicit _erbout.puts calls

(Gavin Kistner) #6

Tricky! I moved the module into ERB::Writeable.

Despite your super calls, I used Object.class_eval to dynamically override #puts, #print and #p at the start of ERB#result, and then undefs those methods at the end of #result. It's sure to be more of a performance hit (though I'll have to benchmark to see how much it affects things) but I wanted to really leave the house as clean when I left as when I entered.

Oh, I also needed to add some .to_s calls inside Writeable's methods, to handle non-string arguments.

In summary (for the google-able archives): using Mark's technique, I have made a patched version of ERB which causes calls to puts, print, and p inside an ERB to place their information into the ERB output string, instead of immediately sending the results to $stdout. (If you use this patch but need some debug information in your ERB template, you can still use $stdout.puts to hide output from the ERB string.)

You can download the patched file from http://phrogz.net/RubyLibs/erb_1.8.2_with_puts.rb.gz
After ungzipping, the file should be renamed to 'erb.rb' and replace the file in (for a standard install):
/usr/local/lib/ruby/1.8/erb.rb

Finally, for the naysayers (and the archive), this functionality (using Ruby code inside an ERB template to inject strings into the ERB template in place without using <%=...%>) can be achieved without the above patch by using the "eoutvar" option for ERB. By default, this is a local variable named "_erbout", but you can name it whatever you wish by modifying the fourth parameter to ERB.new. For example:

require 'erb'

template1 = <<'ENDTEMPLATE'
Hello
<% 1.upto(9){ |i|
     _erbout << "\t#{i} x #{i} = #{i*i}"
     _erbout << "\n" unless i==9
} %>
Goodbye
ENDTEMPLATE
ERB.new( template1 ).run

template2 = <<'ENDTEMPLATE'
Hello
<% 1.upto(9){ |i|
     OUTPUT << "\t#{i} x #{i} = #{i*i}"
     OUTPUT << "\n" unless i==9
} %>
Goodbye
ENDTEMPLATE
ERB.new( template2, nil, nil, 'OUTPUT' ).run

Both of the above output:
Hello
     1 x 1 = 1
     2 x 2 = 4
     3 x 3 = 9
     4 x 4 = 16
     5 x 5 = 25
     6 x 6 = 36
     7 x 7 = 49
     8 x 8 = 64
     9 x 9 = 81
Goodbye

···

On Aug 19, 2005, at 2:07 AM, Mark Hubbart wrote:

How about this? Still somewhat hacky, but maybe less breakable. It
should work unless you supply a binding in the Kernel module context;
the new #puts is defined in Object.