package YAPE::Regex::Explain; use YAPE::Regex 'YAPE::Regex::Explain'; use strict; use vars '$VERSION'; $VERSION = '4.01'; my $exp_format = << 'END'; ^<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< END my $REx_format = << 'END'; ^<<<<<<<<<<<<<<<<<<<<< # ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< END my $noc_format = << 'END'; ^<<<<<<<<<<<<<<<<<<<<< END my ($using_rex,$format,$br); my $valid_POSIX = qr{ alpha | alnum | ascii | cntrl | digit | graph | lower | print | punct | space | upper | word | xdigit }x; my $cc_REx = qr{( \\[0-3][0-7]{2} | \\x[a-fA-F0-9]{2} | \\x\{[a-fA-F0-9]+\} | \\c. | \\[nrftbae] | \\N\{([^\}]+)\} | \\[wWdDsS] | \\([Pp])([A-Za-z]|\{[^\}]+\}) | \[:(\^?)($valid_POSIX):\] | \\?. )}xs; my %modes = ( on => '', off => '' ); my %exp = ( # anchors '\A' => 'the beginning of the string', '^' => 'the beginning of the string', '^m' => 'the beginning of a "line"', '\z' => 'the end of the string', '\Z' => 'before an optional \n, and the end of the string', '$' => 'before an optional \n, and the end of the string', '$m' => 'before an optional \n, and the end of a "line"', '\G' => 'where the last m//g left off', '\b' => 'the boundary between a word char (\w) and something ' . 'that is not a word char', '\B' => 'the boundary between two word chars (\w) or two ' . 'non-word chars (\W)', # quantifiers '*' => '0 or more times', '+' => '1 or more times', '?' => 'optional', # macros '\w' => 'word characters (a-z, A-Z, 0-9, _)', '\W' => 'non-word characters (all but a-z, A-Z, 0-9, _)', '\d' => 'digits (0-9)', '\D' => 'non-digits (all but 0-9)', '\s' => 'whitespace (\n, \r, \t, \f, and " ")', '\S' => 'non-whitespace (all but \n, \r, \t, \f, and " ")', # dot '.' => 'any character except \n', '.s' => 'any character', # alt '|' => "OR", # flags 'i' => 'case-insensitive', '-i' => 'case-sensitive', 'm' => 'with ^ and $ matching start and end of line', '-m' => 'with ^ and $ matching normally', 's' => 'with . matching \n', '-s' => 'with . not matching \n', 'x' => 'disregarding whitespace and comments', '-x' => 'matching whitespace and # normally', ); my %macros = ( # utf8/POSIX macros alpha => 'letters', alnum => 'letters and digits', ascii => 'all ASCII characters (\000 - \177)', cntrl => 'control characters (those with ASCII values less than 32)', digit => 'digits (like \d)', graph => 'alphanumeric and punctuation characters', lower => 'lowercase letters', print => 'alphanumeric, punctuation, and whitespace characters', punct => 'punctuation characters', space => 'whitespace characters (like \s)', upper => 'uppercase letters', word => 'alphanumeric and underscore characters (like \w)', xdigit => 'hexadecimal digits (a-f, A-F, 0-9)', ); my %trans = ( '\a' => q('\a' (alarm)), '\b' => q('\b' (backspace)), '\e' => q('\e' (escape)), '\f' => q('\f' (form feed)), '\n' => q('\n' (newline)), '\r' => q('\r' (carriage return)), '\t' => q('\t' (tab)), ); sub explain { my $self = shift; $using_rex = shift || ''; my $stat = @{[ $self->parse ]}; local $^A = ""; $^A = << "END" if not $using_rex; The regular expression: @{[ $self->display ]} matches as follows: NODE EXPLANATION ---------------------------------------------------------------------- END my @nodes = @{ $self->{TREE} }; $format = $using_rex eq 'silent' ? $noc_format : $using_rex eq 'regex' ? $REx_format : $exp_format; while (my $node = shift @nodes) { $node->explanation; } ($using_rex,$br) = (0,0); %modes = ( on => '', off => '' ); return $^A; } sub YAPE::Regex::Explain::Element::extra_info { my $self = shift; my ($q,$ng) = ($self->quant, $self->ngreed); my $ex = ''; chop $q if $ng; if ($q =~ /\{(\d*)(,?(\d*))\}/) { if ($2 and length $3) { $q = "between $1 and $3 times" } elsif ($2) { $q = "at least $1 times" } elsif (length $1) { $q = "$1 times" } } if ($q) { $ex .= ' (' . ($exp{$q} || $q); $ex .= ' (matching the ' . ($ng ? 'lea' : 'mo') . 'st amount possible)' if $q !~ /^\d+ times$/; $ex .= ')' if $q; } return $ex; } # yes, I'm sure this could be made a bit more efficient... # but I'll deal with the small fish when the big fish are fried sub YAPE::Regex::Explain::Element::handle_flags { my $self = shift; my ($prev_on, $prev_off) = @modes{qw( on off )}; for (split //, $self->{ON}) { $modes{on} .= $_ if index($modes{on},$_) == -1; } my $on = $modes{on} = join "", sort split //, $modes{on}; $modes{off} =~ s/[$on]+//g if length $on; for (split //, $self->{OFF}) { $modes{off} .= $_ if index($modes{off},$_) == -1; } my $off = $modes{off} = join "", sort split //, $modes{off}; $modes{on} =~ s/[$off]+//g if length $off; my $exp = ''; if ($modes{on} ne $prev_on) { for (split //, $modes{on}) { $exp .= ' (' . $exp{$_} . ')' } } if ($modes{off} ne $prev_off) { for (split //, $modes{off}) { $exp .= ' (' . $exp{-$_} . ')' } } return $exp; } sub YAPE::Regex::Explain::anchor::explanation { my $self = shift; my $type = $self->{TEXT}; $type .= 'm' if ($type eq '^' or $type eq '$') and $modes{on} =~ /m/; my $explanation = $exp{$type} . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::macro::explanation { my $self = shift; my $type = $self->text; my $explanation = $exp{$type} . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::oct::explanation { my $self = shift; my $n = oct($self->{TEXT}); my $explanation = "character $n" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::hex::explanation { my $self = shift; my $n = hex($self->{TEXT}); my $explanation = "character $n" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::utf8hex::explanation { my $self = shift; my $n = hex($self->{TEXT}); my $explanation = "UTF character $n" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::ctrl::explanation { my $self = shift; my $c = $self->{TEXT}; my $explanation = "^$c" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::named::explanation { my $self = shift; my $c = $self->{TEXT}; my $explanation = "the character named '$c'" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::Cchar::explanation { my $self = shift; my $c = $self->{TEXT}; my $explanation = "one byte (a C character)" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::slash::explanation { my $self = shift; my $explanation = ($trans{$self->text} || "'$self->{TEXT}'") . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::any::explanation { my $self = shift; my $type = '.'; $type .= 's' if $modes{on} =~ /s/; my $explanation = $exp{$type} . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::text::explanation { my $self = shift; my $text = $self->text; $text =~ s/\n/\\n/g; $text =~ s/\r/\\r/g; $text =~ s/\t/\\t/g; $text =~ s/\f/\\f/g; $text =~ s/'/\\'/g; my $explanation = "'$text'" . $self->extra_info; my $string = $self->string; if ($using_rex) { $string =~ s/\n/\\n/g; $string =~ s/\r/\\r/g; $string =~ s/\t/\\t/g; $string =~ s/\f/\\f/g; $string =~ s/([ #])/\\$1/g; } if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::alt::explanation { my $self = shift; my $explanation = $exp{'|'}; my $string = $self->string; my $oldfmt = $format; $format =~ s/ (\^<+)/$1 /g; $format =~ s/ #/# / if $using_rex; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; $format = $oldfmt; } sub YAPE::Regex::Explain::backref::explanation { my $self = shift; my $explanation = "what was matched by capture \\$self->{TEXT}" . $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::class::explanation { my $self = shift; my $class = $self->{TEXT}; $class = $self->text if $self->{NEG} =~ /[pP]/; my $explanation = "any character "; $explanation .= ($self->{NEG} eq '^') ? "except: " : "of: "; while ($class =~ s/^$cc_REx//) { my ($c1, $name, $pP, $utf8, $neg, $posix) = ($1,$2,$3,$4,$5,$6); if ($name) { $explanation .= qq{the character named "$name"}; } elsif ($utf8) { $utf8 =~ tr/{}//d; (my $nice = $utf8) =~ s/^Is//; my $add = ($pP eq 'P' and "anything but ") . ($macros{lc $nice} || "UTF macro '$utf8'"); $add =~ s/\\([wds])/\\\U$1/ if $pP eq 'P'; $explanation .= $add; } elsif ($posix) { my $add = ($neg and "anything but ") . $macros{lc $posix}; $add =~ s/\\([wds])/\\\U$1/ if $neg; $explanation .= $add; } else { $explanation .= ( $trans{$c1} || ($c1 =~ /\\[wWdDsS]/ and $exp{$c1}) || "'$c1'" ); } if (!$utf8 and !$posix and $c1 !~ /\\[wWdDsS]/ and $class =~ s/^-$cc_REx//) { my ($c2, $name, $pP, $utf8, $neg, $posix) = ($1,$2,$3,$4,$5,$6); $class = "-$c2", next if $utf8 or $posix or $c2 =~ /\\[wWdDsS]/; if ($name) { $explanation .= qq{ to the character named "$name"}; } else { $explanation .= ' to ' . ($trans{$c2} || "'$c2'"); } } $explanation .= ', '; } substr($explanation,-2) = $self->extra_info; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::comment::explanation { } sub YAPE::Regex::Explain::whitespace::explanation { } sub YAPE::Regex::Explain::flags::explanation { my $self = shift; if ($using_rex) { $self->{ON} .= 'x' if $self->{ON} !~ /x/; $self->{OFF} =~ s/x//; } my $string = $self->string; my $explanation = 'set flags for this block' . $self->handle_flags; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::code::explanation { my $self = shift; my $string = $self->string; my $explanation = 'run this block of Perl code'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::later::explanation { my $self = shift; my $string = $self->string; my $explanation = 'run this block of Perl code (that isn\'t interpolated until RIGHT NOW)'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::group::explanation { my $self = shift; if ($using_rex) { $self->{ON} .= 'x' if $self->{ON} !~ /x/; $self->{OFF} =~ s/x//; } my $explanation = 'group, but do not capture' . $self->handle_flags . $self->extra_info . ":"; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my %old = %modes; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{CONTENT} }; $format = $oldfmt; $string = ')' . $self->quant; $explanation = 'end of grouping'; %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::capture::explanation { my $self = shift; my $explanation = 'group and capture to \\' . ++$br . $self->extra_info . ":"; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my %old = %modes; my $old_br = $br; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{CONTENT} }; $format = $oldfmt; $string = ')' . $self->quant; $explanation = "end of \\$old_br"; $explanation .= << "END" if $self->quant; (NOTE: because you are using a quantifier on this capture, only the LAST repetition of the captured pattern will be stored in \\$old_br) END %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::cut::explanation { my $self = shift; my $explanation = 'match (and do not backtrack afterwards)' . $self->extra_info . ":"; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my %old = %modes; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{CONTENT} }; $format = $oldfmt; $string = ')' . $self->quant; $explanation = 'end of look-ahead'; %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::lookahead::explanation { my $self = shift; if (not @{ $self->{CONTENT} }) { my $explanation = ($self->{POS} ? 'succeed' : 'fail') . $self->extra_info; my $string = $self->fullstring; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; return; } my $explanation = 'look ahead to see if there is' . ($self->{POS} ? '' : ' not') . $self->extra_info . ":"; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my %old = %modes; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{CONTENT} }; $format = $oldfmt; $string = ')' . $self->quant; $explanation = 'end of look-ahead'; %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::lookbehind::explanation { my $self = shift; my $explanation = 'look behind to see if there is' . ($self->{POS} ? '' : ' not') . $self->extra_info . ":"; my $string = $self->string; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my %old = %modes; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{CONTENT} }; $format = $oldfmt; $string = ')' . $self->quant; $explanation = 'end of look-behind'; %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } sub YAPE::Regex::Explain::conditional::explanation { my $self = shift; my ($string,$explanation); if (ref $self->{CONTENT}) { $string = '(?'; $explanation = 'if the following assertion is true' . $self->extra_info . ":"; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $self->{CONTENT}[0]->explanation; $format =~ s/ (\^<+)/$1 /g; $format =~ s/ #/# / if $using_rex; $explanation = 'then:'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; $format = $oldfmt; } else { $string = $self->string; $explanation = "if back-reference \\$self->{CONTENT} matched, then" . $self->extra_info . ":"; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } my %old = %modes; my $oldfmt = $format; $format =~ s/\^<<(<+)/ ^$1/g; $format =~ s/# / #/ if $using_rex; $_->explanation for @{ $self->{TRUE} }; unless (@{ $self->{TRUE} }) { my $string = ""; my $explanation = 'succeed'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } { my $oldfmt = $format; $format =~ s/ (\^<+)/$1 /g; $format =~ s/ #/# / if $using_rex; my $string = "|"; my $explanation = 'else:'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; $format = $oldfmt; } $_->explanation for @{ $self->{FALSE} }; if (not @{ $self->{FALSE} }) { my $string = ""; my $explanation = 'succeed'; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } $format = $oldfmt; $string = ')' . $self->quant; $explanation = "end of conditional" . (ref $self->{CONTENT} ? '' : " on \\$self->{CONTENT}"); %modes = %old; if ($using_rex ne 'silent') { formline($format, $string, $explanation) while length($string . $explanation) } else { formline($format, $string) while length($string) } $^A .= ($using_rex ? '' : '-' x 70) . "\n"; } 1; =head1 NAME YAPE::Regex::Explain - explanation of a regular expression =head1 VERSION This document refers to YAPE::Regex::Explain version 4.01. =head1 SYNOPSIS use YAPE::Regex::Explain; my $exp = YAPE::Regex::Explain->new($REx)->explain; =head1 C MODULES The C hierarchy of modules is an attempt at a unified means of parsing and extracting content. It attempts to maintain a generic interface, to promote simplicity and reusability. The API is powerful, yet simple. The modules do tokenization (which can be intercepted) and build trees, so that extraction of specific nodes is doable. =head1 DESCRIPTION This module merely sub-classes C, and produces a rather verbose explanation of a regex, suitable for demonstration and tutorial purposes. =head2 Methods for C =over 4 =item * Cnew($regex);> Calls C's C method (see its docs). =item * Cexplain($mode);> Returns a string explaining the regex. While not required for all regexes, it is sometimes necessary to compile the regex using the C operator before passing it to the C method. If C<$mode> is C, it will output a valid regex (instead of the normal string). If C<$mode> is C, no comments will be added, but the regex will be expanded into a readable format. =back =head1 EXAMPLES Print the full explanation for the regex C<\Q[abc]\E\d+>, compiling it first: print YAPE::Regex::Explain->new(qr/\Q[abc]\E\d+/)->explain(); Print the explanation for the regex C<\w[a-f]*>, without comments: print YAPE::Regex::Explain->new('\w[a-f]*')->explain('silent'); Print the explanation for a multi-line regex: my $re = qr{ (foo|bar) # just a comment \d+ / }ix; print YAPE::Regex::Explain->new($re)->explain(); =head1 LIMITATIONS There is no support for regular expression syntax added after Perl version 5.6, particularly any constructs added in 5.10. For examples, refer to: L =head1 DEPENDENCIES L =head1 AUTHOR The original author is Jeff "japhy" Pinyan (CPAN ID: PINYAN). Gene Sullivan (gsullivan@cpan.org) is a co-maintainer. =head1 LICENSE This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L. =cut