47

I'd like to temporarily redirect stderr in a Ruby script for the duration of a block, ensuring that I reset it to its original value at the end of the block.

I had trouble finding how to do this in the ruby docs.

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
horseyguy
  • 29,455
  • 20
  • 103
  • 145
  • 1
    Related question: http://stackoverflow.com/questions/3018595/how-do-i-redirect-stderr-and-stdout-to-file-for-a-ruby-script – Andrew Grimm Apr 07 '11 at 02:26

4 Answers4

73

In Ruby, $stderr refers to the output stream that is currently used as stderr, whereas STDERR is the default stderr stream. It is easy to temporarily assign a different output stream to $stderr.

require "stringio"

def capture_stderr
  # The output stream must be an IO-like object. In this case we capture it in
  # an in-memory IO object so we can return the string value. You can assign any
  # IO object here.
  previous_stderr, $stderr = $stderr, StringIO.new
  yield
  $stderr.string
ensure
  # Restore the previous value of stderr (typically equal to STDERR).
  $stderr = previous_stderr
end

Now you can do the following:

captured_output = capture_stderr do
  # Does not output anything directly.
  $stderr.puts "test"
end

captured_output
#=> "test\n"

The same principle also works for $stdout and STDOUT.

molf
  • 73,644
  • 13
  • 135
  • 118
  • I'm using this on a function (RubyVM::InstructionSequence) that directly writes to stderr (file descriptor 2) in C. Will this work for that?. I am trying to suppress the stderr output of a low level C function called by the Ruby runtime – horseyguy Dec 16 '10 at 10:59
  • 1
    @banister: Yes, it should work; Ruby uses `$stderr` internally. And if for whatever reason it's not the case (which would be a bug) then unfortunately you won't be able to do anything about it in your Ruby code. – molf Dec 16 '10 at 11:15
  • this does not work in ruby18. which ruby has the `StringIO` library? – Igbanam Sep 19 '12 at 19:28
  • This is great, but does not work in the case where the value of `$stderr` was passed or copied before the reassignment was done. In contrast, `reopen` will work on all copies of `$stderr`. The problem is that passing a `StringIO` to `reopen` does not work, I get this error: "TypeError: no implicit conversion of StringIO into String". – Keith Bennett May 05 '21 at 18:57
19

Here is a more abstract solution (credit goes to David Heinemeier Hansson):

def silence_streams(*streams)
  on_hold = streams.collect { |stream| stream.dup }
  streams.each do |stream|
    stream.reopen(RUBY_PLATFORM =~ /mswin/ ? 'NUL:' : '/dev/null')
    stream.sync = true
  end
  yield
ensure
  streams.each_with_index do |stream, i|
    stream.reopen(on_hold[i])
  end
end

Usage:

silence_streams(STDERR) { do_something }
user2398029
  • 6,699
  • 8
  • 48
  • 80
  • My RUBY_PLATFORM == "i386-mingw32", so this would not work on my Windows machine. You never know what weird environments people may be running, so I'd recommend against string matching for this. – Nossidge Sep 28 '17 at 02:41
10

Essentially the same as @molf's answer, and has the same usage:

require "stringio"
def capture_stderr
  real_stderr, $stderr = $stderr, StringIO.new
  yield
  $stderr.string
ensure
  $stderr = real_stderr
end

It uses StringIO very slightly more concisely, and preserves $stderr as whatever it was before capture_stderr was called.

Peter Marreck
  • 53
  • 1
  • 3
gunn
  • 8,999
  • 2
  • 24
  • 24
7

I like the StringIO answers. But if you are calling an external process, and $stderr = StringIO.new doesn't work, you might write stderr out to a temporary file:

require 'tempfile'

def capture_stderr
  backup_stderr = STDERR.dup
  begin
    Tempfile.open("captured_stderr") do |f|
      STDERR.reopen(f)
      yield
      f.rewind
      f.read
    end
  ensure
    STDERR.reopen backup_stderr
  end
end
zhon
  • 1,610
  • 1
  • 22
  • 31
  • 1
    Thanks for this solution! I wrote [my own solution](https://stackoverflow.com/a/57394873/601203) based on it by adding documentation and removing the temporary file. – hagello Aug 07 '19 at 13:44