NAME
Filter::Cleanup - A stackable way to deal with error handling
VERSION
Version 0.01
SYNOPSIS
use Filter::Cleanup;
use Filter::Cleanup debug => 1;
sub foo {
my $file_path = shift;
open my $FH, $file_path or die $!;
cleanup { close $FH };
do_risky_stuff_with_fh($FH);
}
sub html {
# cleanups stack and execute in reverse order
cleanup { print "</html>" };
cleanup { print "</body>" };
print "<html>\n";
print "<head><title>Test page</title></head>\n";
print "<body>\n";
print generate_page_body();
}
DESCRIPTION
Filter::Cleanup provides a simple way to deal with cleaning up after multiple error conditions modeled after the D programming language's scope(exit)
mechanism.
Each cleanup
block operates in essentially the same manner as a finally
block in languages supporting try/catch/finally style error handling.
cleanup
blocks may be placed anywhere in a scope. All statements lexically scoped after the cleanup
block will be wrapped in an eval
. Should an error be triggered within the block, the cleanup
statement will be called before any error is rethrown (using croak
).
Within the cleanup
block, the status of $@
may be inspected normally.
Multiple cleanup
blocks stack, and each MUST be followed by a semi-colon to ensure proper organization of the outputted code. cleanup
s are executed in reverse order (it's a stack, see?) and may be nested, although this defeats the purpose. The reason for reverse execution is that each cleanup represents another nested level of evals and clean-up code.
Take the following code:
use Filter::Cleanup;
sub example {
cleanup { print "FOO" };
print "BAR";
return 1;
}
This is roughly the output of the source filter:
sub example {
my $result = eval {
print "BAR";
return 1;
};
my $error = $@;
print "FOO";
if ($error) {
croak $error;
} else {
$result; # returns 1
}
}
Now with multiple cleanups:
use Filter::Cleanup;
sub example {
cleanup { print "FOO" };
cleanup { print "BAZ" };
print "BAR";
return 1;
}
The following code would be generated:
sub example {
my $result = eval {
my $result = eval {
print "BAR";
return 1;
};
my $error = $@;
print "BAZ";
if ($error) {
croak $error;
} else {
$result;
}
};
my $error = $@;
print "FOO";
if ($error) {
croak $error;
} else {
$result; # returns 1
}
}
Internally, PPI is used to parse the module and generate the new code. This is because there are so many different forms which could proceed a cleanup
block that there is no more efficient way to ensure that valid code is emitted. PPI has proven to be stable, robust, and very reasonably efficient.
Modifying return variables within a cleanup block
This can sometimes have surprising results due to the manner in which cleanup
blocks are evaluated. By the time the cleanup
block executes, the result of evaluating the protected code has already been determined and stored. Cleanup
blocks are then processed, and their results are discarded after being inspected for errors. Therefore, something like this:
sub test {
my @words = ('foo');
cleanup { push @words, 'bat' };
cleanup { push @words, 'baz' };
cleanup { push @words, 'bar' };
return @words;
}
...will cause 'foo' to be returned, because @words has not been modified by the time the return value is calculated.
In order to effect changes in return values in cleanup (a questionable practice, but hey, I don't judge), a reference is required:
sub test {
my $words = ['foo'];
cleanup { push @$words, 'bat' };
cleanup { push @$words, 'baz' };
cleanup { push @$words, 'bar' };
return $words;
}
The above code will return ['foo', 'bar', 'baz', 'bat']
.
SUBROUTINES
- import
-
Importing Filter::Cleanup makes the
cleanup
keyword available in the importing scope. Addingdebug=
1> will cause the generated code to be printed toSTDERR
with line numbers. - filter
-
Expected by Filter::Util::Call; provides the entry point into the source filter.
- _transform
-
Performs the actual work of modifying the source.
AUTHOR
Jeff Ober mailto:jeffober@gmail.com
LICENSE
BSD license