Home > Net >  How can I capture the output of Perl string eval?
How can I capture the output of Perl string eval?

Time:09-17

I have a piece of code that is formed through string processing and I am trying to run an eval to capture the output of the code into another string. The piece of code is formed some steps itself, which means it's not in body of Perl program in one piece. This is why I am using eval.

I tried using Tiny::Capture without capture, capture_merged, capture_stdout and also Safe with Safe->new->reval().

use strict;
use warnings;
use Capture::Tiny 'capture';
use Capture::Tiny 'capture_stdout';
use Capture::Tiny 'capture_merged';

# $num_loops is defined inside main code body.
my $num_loops = 10;

#In reality, new code is formed through a series of steps.
my $new_code = "foreach \$idx (0..\$num_loops-1) {print \"I am at iteration number \$idx\n\";}";

my $out = capture_merged {eval $new_code;};
print $out;

I'm using Perl 5.30.

The piece of code above isn't printing anything out. Neither does it print any error in log. I have given intermediate prints in original code to make sure the string thrown into eval has real code, so that's not an issue.

What is the issue in the code above?

CodePudding user response:

Documentation is written for a reason, you should not assume return value from a function -- documentation gives details what is a return value.

Sample code how you could use eval for your situation.

use strict;
use warnings;

my $num_loops = 10;
my $new_code = '
    my $r;
    $r .= "I am at iteration number $_\n" for 0..$num_loops-1;
    return $r;
';

my $out = eval $new_code;

print $out;

Output

I am at iteration number 0
I am at iteration number 1
I am at iteration number 2
I am at iteration number 3
I am at iteration number 4
I am at iteration number 5
I am at iteration number 6
I am at iteration number 7
I am at iteration number 8
I am at iteration number 9

CodePudding user response:

If I understand you correctly, you want to do the same as the

$var = `... commands ...`;
$var = qx(.. commands ..);

but with commands being perl instead of shell code, and without forking a separate process.

You can do that by localizing STDOUT and re-opening the local filehandle as an in-memory file.

Simple example, without any external module:

use strict;
sub qp(&){
    my $s;
    open local *STDOUT, '>', \$s or die "open in-memory file: $!";
    &{$_[0]};
    die $@ if $@;
    $s;
}

# Usage:
my $a = qp { eval 'print "in"' };
print "<$a>\n";
print "out\n";
# 'in' will go into the string, 'out' to the actual stdout

# Use with your example (fixed to use 'my $idx'):
my $num_loops = 10;
my $new_code = "foreach my \$idx (0..\$num_loops-1) {print \"I am at iteration number \$idx\n\";}";
my $out = qp { eval $new_code };
print $out;

The limitations of this are

  1. that will not catch syswrite output -- that could be easily fixed by tieing the localized STDOUT and implementing PRINT, PRINTF and WRITE to append to a string.
  2. that will not catch the output from separate processes spawn by perl -- that could only be fixed by re-opening STDOUT to a temporary file (that's what Capture::Tiny does under the hood).

Why your example doesn't work

In your eval'd code, you're using foreach $idx (...) without $idx being defined, and since you're within the scope of use strict, that errors out, and your code is not run. But you're not checking the value of $@, and you're not able to see that error. Change it to foreach my $idx (...) as I did above.

  • Related