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. cleanups 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. Adding debug=1> will cause the generated code to be printed to STDERR 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