How to Temporarily Redirect Stderr in Ruby

How do I temporarily redirect stderr in Ruby?

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.

How to redirect STDOUT and STDERR in Ruby and make them equal?

2.1.3 :005 > %x{(ls -d /boot 2>&1) | tee /dev/stderr}
/boot
=> "/boot\n"

2.1.3 :006 > %x{(ls /no_such_file 2>&1) | tee /dev/stderr}
ls: cannot access /no_such_file: No such file or directory
=> "ls: cannot access /no_such_file: No such file or directory\n"

How to turn off $stdout and $stderr in Ruby

This gist will show you how to redirect to /dev/null:

https://gist.github.com/moertel/11091573

Or this SO answer:

How do I redirect stderr and stdout to file for a Ruby script?

Your question probably needs to be marked as a dup.

How can I log STDOUT and STDERR to a single file and show only STDERR in the console using Ruby?

This Ruby script satisfies my needs, but maybe there is a better way.

logPath = ARGV[0]
logFolder = File.dirname(logPath)
command = ARGV.slice(1..-1).join(" ")
`mkdir -p #{logFolder}`
exec "#{command} 2>&1 >> #{logPath} | tee -a #{logPath}"

Side effects of redefining $stdout and $stderr in a Rails app

The standard output and standard error streams are generally accessible in two ways each:

  • $stdout and STDOUT
  • $stderr and STDERR

A sufficiently clever person could also open their own copies using IO.new with a file descriptor argument:

sneaky = IO.new(2, 'w')

And now you have writable access to the standard error stream through sneaky without having anything to do with $stderr or STDERR.

Reassigning $stderr and $stdout should work fine unless something in your code, your gems, or Ruby itself is using the constants (STDOUT, STDERR) or is accessing the streams directly through C's stdio, unix's low level read/write with numeric file descriptors or is opening their own access to the streams using IO.new. I haven't dug into the source but I doubt assigning to $stdout will do anything to stdout in C-land or file descriptor 1 in Unix-land.

If you really need to trap the standard output and error streams then you're probably better off writing a wrapper shell script to redirect the streams for you.


UPDATE: If you're mostly (only?) concerned about changing $stdout and $stderr inside your script/runner bleeding into the rest of your Rails app then you don't have to worry about that. Each process — your script/runner process and however many server processes your main application has running — gets its own set of globals so you can change them all you want in your script without making a mess of your main application. Of course, you still have to worry about gems using STDOUT instead of $stderr or using IO.new to get their own private standard out.

Redirect stderr to Logger instance

If you're creating the logger yourself, you can create the File object first, then use it to create the logger and assign it to $stderr:

log_file = File.new('foo.log', 'a')
logger = Logger.new(log_file, 'weekly')
$stderr = log_file #usually no need to use reopen

Note that this will result in the log output being mixed up with the output of $stderr, which may cause problems if you're parsing the log file expecting it to be in a certain format (this will happen with your solution too).

If you don't have the underlying file but just receive the logger from somewhere else, it's a bit more tricky. What is needed is an IO like object that can be assigned to $stderr and passes anything written to it to the logger. The IO class in Ruby is unfortunately fairly closely tied to the underlying i/o system (file descriptors and the like), and there's no general interface that can be used to create input and output streams. (StringIO being the notable exception).

However most, if not all, of the output methods on IO ultimately go through #write, so by overriding this one method you can get close to what you're after:

class IOToLog < IO

def initialize(logger)
@logger = logger
end

def write(string)
#assume anything written to stderr is an error
@logger.error(message)
end

end

logger = get_logger_from_somewhere

$stderr = IOToLog.new(logger)

Now anything written to $stderr will end up going to the log file. The formatting however will be a bit odd. Anytime any of the writing methods calls #write a new entry will be made in the logfile. For example, #puts called with an array will call #write for each entry of the array, and again with a newline character between each entry, resulting in 2n - 1 log entries, n - 1 of which will be blank.

You could make the overridden #write method more complex to handle this, perhaps using an internal buffer, and only call the logger when you think you have a full message. Alternatively you could override the individual methods to write to the logger themselves. If you did this the IOToLog class wouldn't necessarily have to inherit from IO.

Your best solution will depend on how you want to standard error output to appear in the logfile, how your program uses $stderr, and how much work you want to do implementing methods from IO.

Ruby capture stderr output from bash script execution

Simply redirect it:

val = %x[ #{cmd} 2>&1 ]

If you want to capture output from stderr only, close the file descriptor for stdout after copying it to fd 2.

val = %x[ #{cmd} 2>&1 >/dev/null ]


Related Topics



Leave a reply



Submit