use FileHandle;
use Getopt::Std;
use POSIX ":sys_wait_h";
my $gPrefix;
my $gVerbose;
my $gSpecials = "()[]{}\$\@";
my $gEscapes = "";
my %gSpecmap;
exit(&main());
sub escape {
my ($line) = @_;
if (length($gSpecials) and !length($gEscapes)) {
for (split //, $gSpecials) {
my ($spc) = $_;
my ($esc) = "\\$spc";
$gSpecmap{$esc} = $spc;
$gSpecmap{$spc} = $esc;
$gEscapes .= $esc;
}
}
$line =~ s/ (
(\\[$gEscapes]) |
([$gEscapes]) )
/$gSpecmap{$1}/gx;
return $line;
}
sub read_checkfile {
my ($path, $prefix) = @_;
my @checks;
my $file = FileHandle->new($path, "r");
foreach my $line (<$file>) {
chomp $line;
if ($line =~ /\s*\ my $rule = defined($3) ? $3 : "IS";
my $pred = defined($4) ? $4 : "";
$pred = "" if ($rule eq "EMPTY");
$pred =~ s/^\s*//;
$pred =~ s/\s*$//;
$pred = &escape($pred);
if ($gVerbose) {
printf " RULE : '$rule':'$pred'\n";
}
push @checks, "$rule:$pred";
}
}
return \@checks;
}
sub apply_pattern_to_line {
my ($rule, $pred, $line) = @_;
my $res;
if ($rule eq "EMPTY") {
$res = $line eq "";
if ($gVerbose and $res) {
print " MATCH: '$rule'\n";
}
}
else {
$res = $line =~ /^\s*${pred}\s*$/;
if ($gVerbose and $res) {
print " MATCH: '$rule':'$pred'\n";
}
}
return $res;
}
sub apply_fail {
my ($rule, $pred, $line, $linesRef, $iline) = @_;
my $pattern = $rule eq "EMPTY" ? "" : " '$pred'";
my $label = ($rule eq "IS") ? "$gPrefix" : "$gPrefix-$rule";
print STDERR "error: failed to match ${label}:${pattern}\n";
if ($line) {
print STDERR " expected: '$pred'\n"
unless ($rule eq "NOT" or $rule eq "EMPTY");
}
if (defined($linesRef)) {
my ($nline) = scalar @$linesRef;
while (++$iline < $nline) {
my ($line) = $linesRef->[$iline];
print STDERR " LINES: '$line'\n";
}
}
return 1;
}
sub apply_checks_to_lines {
my ($checksRef, $linesRef) = @_;
my $ncheck = scalar @$checksRef;
my $icheck = 0;
my $nline = scalar @$linesRef;
my $iline = 0;
my $ilast = -1;
while ($iline < $nline)
{
my $line = $linesRef->[$iline];
if ($gVerbose) {
print " LINE : '$line'\n";
}
for (my $jcheck = $icheck; $jcheck < $ncheck; ++$jcheck)
{
my $check = $checksRef->[$jcheck];
$check =~ /([^:]*):(.*)/;
my $rule = $1;
my $pred = $2;
if ($rule eq "IS") {
if (&apply_pattern_to_line($rule, $pred, $line)) {
$icheck = $jcheck + 1;
$ilast = $iline;
}
last;
}
elsif ($rule eq "NEXT") {
if (&apply_pattern_to_line($rule, $pred, $line)) {
$icheck = $jcheck + 1;
$ilast = $iline;
last;
}
else {
return &apply_fail($rule, $pred, $line, $linesRef, $ilast);
}
}
elsif ($rule eq "NOT") {
if (&apply_pattern_to_line($rule, $pred, $line)) {
return &apply_fail($rule, $pred, $line, $linesRef, $ilast);
}
}
elsif ($rule eq "EMPTY") {
unless (&apply_pattern_to_line($rule, $pred, $line)) {
return &apply_fail($rule, $pred, $line, $linesRef, $ilast);
}
$icheck = $jcheck + 1;
last;
}
else {
printf STDERR "unkown rule type: $rule\n";
return -1;
}
}
if ($icheck >= $ncheck) {
last
}
$iline += 1;
}
for (my $jcheck = $icheck; $jcheck < $ncheck; ++$jcheck)
{
my $check = $checksRef->[$jcheck];
$check =~ /([^:]*):(.*)/;
my $rule = $1;
my $pred = $2;
unless ($rule eq "NOT") {
return &apply_fail($rule, $pred, undef, $linesRef, $ilast);
}
}
return 0;
}
sub self_test_fork {
my ($test) = @_;
print "self_test: $test\n";
my $testInput = new FileHandle or die "can't create write handle";
my $testOutput = new FileHandle or die "can't create read handle";
pipe my $childInput, $testInput or die "can't create read handle";
pipe $testOutput, my $childOutput or die "can't create write handle";
my $pid = fork();
unless ($pid) {
close $testInput;
close $testOutput;
open(STDIN, "<&=" . fileno($childInput)) or die "can't redirect stdin";
open(STDOUT, "<&=" . fileno($childOutput)) or die "can't redirect stdout";
open(STDERR, "<&=" . fileno($childOutput)) or die "can't redirect stderr";
my $v = $gVerbose ? "-v" : "";
exec "$0 $v -T -p $test $0";
die "unreachable";
}
close $childInput;
close $childOutput;
print $testInput "A\n";
print $testInput "B\n";
print $testInput "C\n";
print $testInput "D\n";
print $testInput "\n";
print $testInput "E\n";
close $testInput;
wait;
my $status = $?;
$status = $status == -1 ? $status : $status >> 8;
while (<$testOutput>) {
print;
}
close $testOutput;
if ($status and $test =~ /ERR/) {
$status = 0;
printf "error is expected, passing.\n";
}
return $status;
}
sub self_test {
my ($unused) = @_;
for my $test ( "MATCH", "MATCHERR", "NEXT", "NEXTERR", "NOT", "NOT2",
"NOTERR", "EMPTY", "EMPTYERR" ) {
if (&self_test_fork($test)) {
print STDERR "FAIL\n";
return 1;
}
}
return 0;
}
sub escape_file {
my ($fh) = @_;
my ($hex) = "[[:xdigit:]]";
my ($ehex) = &escape($hex);
my ($next) = "";
my ($specials) = "*+?";
for my $line (<$fh>) {
$line =~ s/([$specials])/\\$1/g;
if ($opt_x) {
$line =~ s/${hex}{4,}/$ehex\+/g;
}
print "# ${opt_p}${next}: $line";
$next = "-NEXT" unless($next);
}
}
sub main {
return &usage() unless (@ARGV);
$opt_p = "CHECK";
$opt_x = undef;
my (@files);
while (scalar @ARGV) {
return &usage() unless getopts('ei:p:tTvx');
push @files, shift @ARGV if (scalar @ARGV);
}
$gPrefix = $opt_p;
$gVerbose = $opt_v;
@ARGV = @files;
if ($opt_t and $opt_e) {
print STDERR "error: -e and -t may not be combined\n";
return 1;
}
if ($opt_t) {
&usage() if ($opt_t or $opt_p or scalar @ARGV);
return &self_test($self_test);
}
unless ($opt_e) {
return &usage() unless (scalar @ARGV == 1);
}
if (defined($opt_T)) {
my $old_fh = select(STDOUT);
$| = 1;
select(STDERR);
$| = 1;
select($old_fh);
}
my $file = shift @ARGV;
unless ($opt_e) {
unless ( -f "$file" ) {
print STDERR "error: file not found: $file\n";
return 1;
}
}
if ($opt_i) {
unless ( -f "$opt_i" ) {
print STDERR "error: file not found: $opt_i\n";
return 1;
}
}
my $input_fh = $opt_i ? FileHandle->new($opt_i, "r") :
FileHandle->new_from_fd(0, "r");
unless ($input_fh) {
if ($opt_i) {
print STDERR "error: cannae read $opt_i\n";
return 1;
} else {
print STDERR "error: cannae read STDIN?\n";
return 1;
}
}
if ($opt_e) {
return &escape_file($input_fh);
}
$gPrefix = $opt_p;
$gVerbose = $opt_v ? $opt_v : "";
my $checksRef = &read_checkfile($file, $opt_p);
die "error: no ${opt_p} lines found in checkfile\n"
unless(scalar @$checksRef);
my $firstCheck = @$checksRef[0];
$firstCheck =~ /([^:]*):(.*)/;
my $rule = $1;
die "error: first rule cannot be '-NEXT'\n"
if ($rule eq "NEXT");
my @input;
while (<$input_fh>) {
chomp;
push @input, $_;
}
my $res = &apply_checks_to_lines($checksRef, \@input);
if ($res and !defined($opt_T)) {
print STDERR "FAIL\n";
}
return $res ? 1 : 0;
}
sub usage {
(my $basename = $0) =~ s|.*/||;
print <<USAGE;
usage: $basename [-i <input>] [-p <prefix>] [-v] checkfile
$basename [-i <input>] [-p <prefix>] -e [-x]
$basename -t [-v]
-e - write out input with a test prefix appended to each line.
-i <input> - read the input under test from <input> rather than STDIN.
-p <prefix> - use <prefix> instead of CHECK for rule labels.
-t - run $basename in "self-test" mode.
-v - print verbose output from the rules matching engine.
-x - used with -e. escape large hexadecimal numbers.
USAGE
return 1;
}