Capture stdout from a function?

Hi,

is there a way to capture stdout from a function?

Like this in Perl:

#!/usr/bin/env perl                                                                              
                                                                                                 
use strict;                                                                                      
use warnings;                                                                                    
use feature qw(say);                                                                             
use Capture::Tiny qw(capture);

my $fn = sub {print shift};

say ((capture {$fn->("fubar")})[0]);

__END__                                                                                          
                                                                                                 
karl@rantanplan:~/src/crystal$ ./capture.pl                                                      
fubar                                                                                          

Regards, Karl

Update:

Thanks to all for the kind and helpful replies.

I just searched a cheap trick to capture output from a C function “in-a-hurry”.

Now i know that this doesn’t work for a function that returns void - Crystal is too clever.

I also know now that i can’t pass a struct
defined in a C lib from Crystal (via new) to the function call.

I had the same idea as Billy:

26 | fn(io, GMP.fac(3))
                 ^--
Error: passing Void return value of lib fun call has no effect

Same result with the solution from Jamie, as expected.

In the packt book from Dietrich are some examples with callbacks…

I think it would be better if you have your method accept an IO and interact with that. Then you could either pass STDOUT, or an IO::Memory in order to access what was printed.

1 Like

Crystal doesn’t provide a way to hijack STDOUT within the program but, since STDOUT is just another IO, anything that writes to it can be configured to write to a different IO object instead which can be intercepted using IO::MultiWriter. Example:

def foo(output : IO = STDOUT)
  String.build do |str|
    yield IO::MultiWriter.new(io, str)
  end
end

buffer = foo do |output|
  10.times do |i|
    output.puts "hello #{i}"
  end
end

pp! buffer
# buffer # => "hello 0\n" +
# "hello 1\n" +
# "hello 2\n" +
# "hello 3\n" +
# "hello 4\n" +
# "hello 5\n" +
# "hello 6\n" +
# "hello 7\n" +
# "hello 8\n" +
# "hello 9\n"

String.build yields a writable IO instance that can be used just like any other and the return value is a string containing all of the data that was written to that IO instance. So we can pass that to IO::MultiWriter.new and receive a copy of everything.

1 Like

I agree with what @Blacksmoke16 and @jgaskins suggests. But if you need to capture STDOUT/STDIN of the running process you can check GitHub - mosop/stdio: A small Crystal library for capturing standard I/O streams., probably some of the forks with some updates since that shards is from 0.x era IIRC.

3 Likes

Having something like that (maintained) for tests would be super nice.

1 Like

I’d argue it would be better to just do something like def run(output : IO = STDOUT) as the entry point into your application and not have to do any of this kind of stuff at all.

2 Likes

I do that. OTOH, not all libraries do that.

1 Like

I write an answer similar your Perl code.

  1. change you fn to accept a IO object as first args

def fn(io, string)
    io.print string
end
  1. passing an IO::Memory object into
io = IO::Memory.new
fn(io, "fubar")

puts io.to_s # => "fubar"
1 Like

2 posts were split to a new topic: 9 Ways to Run System Commands in Ruby