How can I use IO#reopen with a StringIO?

Viewed 167

I would like to capture stderr into a variable in memory, without using a file on the filesystem. (This is because if the process is kill -9'ed, the file, even if it is a Tempfile, will not be deleted.)

There is a solution for this at How do I temporarily redirect stderr in Ruby?, but its strategy is to assign a StringIO to $stderr. This will not work if the value of $stderr was copied prior to the reassignment, nor will it work if STDERR is used. As evidence:

#!/usr/bin/env ruby

stderr_sav = $stderr
$stderr = File.open(File::NULL, 'w')
$stderr.puts    'Using $stderr'
stderr_sav.puts 'Using stderr_sav'
STDERR.puts     'Using STDERR'

# Outputs:

# Using stderr_sav
# Using STDERR

In contrast, using reopen works:

#!/usr/bin/env ruby

stderr_sav = $stderr
$stderr.reopen(File.new(File::NULL, 'w'))
$stderr.puts    'Using $stderr'
stderr_sav.puts 'Using stderr_sav'
STDERR.puts     'Using STDERR'

# Outputs:

# [nothing]

Unfortunately, passing a StringIO to reopen does not work:

`reopen': no implicit conversion of StringIO into String (TypeError)

Is there any way to accomplish the capturing of $stderr without using a file?

1 Answers

How about a proxy which temporary delegate IO#puts to StringIO#puts

stderr_sav = $stderr

# delegate
$stdclone = $stderr.clone
$temp = StringIO.new

[:puts].each do |m|
  if $temp.respond_to?(m)
    $stderr.define_singleton_method "#{m}" do |*args, &block|
      $temp.send(m, *args, &block)
    end
  end
end

# test
$stderr.puts 'secret1' # nothing
STDERR.puts 'secret2' # nothing
stderr_sav.puts 'secret3' # nothing

new_strerr = IO.new(1)
new_strerr.puts "not secret" # not secret

$stdclone.puts $temp.string # secret 1 -> 3

# reset
$stderr = $stdclone
$stderr.puts "secret4" # secret4
Related