How to "tee" I/O in a generic fashion

Trying to be a little fancy but can’t figure out how to do it right.
Basically I want to redirect STDOUT and STDERR so that they each
continue to write to their default streams but also write to a log file.
The goal is that ALL of the rest of the code can be completely oblivious
and continue to write to STDOUT and STDERR as normal and the Tee-ing
will work automatically.

I tried the following simple experiment:

class Tee < IO
    def initialize (*fhs)
        @fhs = fhs
    end

    def write (string)
        @fhs.each do |fh|
            fh.print string
        end
    end
end

$stdout = Tee.new($stdout)

puts "Hello world"

system("pwd")

I get the following:

Hello world
bar.rb:21:in `system': uninitialized stream (IOError)

from bar.rb:21:in `’

So the native Ruby I/O seems to work but somehow the system command does
not like this. I suspect what’s going on is that the native Ruby I/O
commands have logic to handle a true stream and a virtual stream like
this differently, whereas the system command does not. Yes, I know that
I could rewrite the system call to capture the output explicitly and use
print to write it out but, again, the point of this experiment is to
capture any and I/O and be agnostic of how the code is written. I could
use pipes and such but I’m sure there must be a more elegant way to do
this.

Any help is appreciated.

Thanks.

Just a guess:

With your approach, you do not really handle with “stdout” (as seen by
the operating system). You only modify the meaning of the variable
$stdout. This goes well, while you are in the realm of Ruby, i.e. of
those methods, which use $stdout for writing. A method such as system
must deal with the concept of stdout as being imposed by the operating
system. The writers of system()certainly did not anticipate that someone
will change the variable $stdout. It is not surprising that at least
something will likely go wrong.

I would expect problems in a different place too: The standard output in
Rub is represented by $stdout and STDOUT. You redefined the former.
Unless STDOUT is represented somehow as a reference to whatever $stdout
points to - and I don’t think this is the case - any output going to
STDOUT would not be tee’d. Try it.