#!/usr/bin/perl
# grok is released under the Creative Commons Attribution 2.5 License
# If you care, an overview of this license (and the legalese) is at:
# http://creativecommons.org/licenses/by/2.5/
#
# Email jls@csh.rit.edu if you have any problems or questions with using grok.
use strict;
use warnings;
use Data::Dumper;
use IO::Select;
use Getopt::Std;
use POSIX qw(setsid strftime);
use Memoize;
use Socket;
# For older versions of perl.. but do I really care that much?
use Symbol;
# allow (?{ code }) zero-width assertions in dynamically generated regexen.
# This let's me do named captures with $^N
use re 'eval';
# Things you'll need to have installed (from CPAN):
use URI::Escape;
use Regexp::Common qw(RE_ALL);
use Parse::RecDescent;
use Date::Parse;
use Unix::Syslog qw(:macros :subs);
# Memoization of pattern2regex increases speed by a factor of 2.5
memoize('pattern2regex');
my @logrefs;
my $debuglevel = 0;
my $PATTERNS;
# Pattern-space wildcards, used as %WILDCARD% in the pattern config
# use 'our' so require()'d sources can access us.
our %MATCH = (
USERNAME => qr/[a-zA-Z0-9_-]+/,
USER => "%USERNAME%",
INT => qr/$RE{num}{int}/,
# Regex::Common's real number matches literal "." which is bad.
#NUMBER => qr/$RE{num}{real}/,
NUMBER => qr/(?:[+-]?(?:(?:[0-9]+(?:\.[0-9]*)?)|(?:\.[0-9]+)))/,
POSITIVENUM => qr/\b[0-9]+\b/,
WORD => qr/\w+/,
NOTSPACE => qr/\S+/,
DATA => qr/.*?/,
GREEDYDATA => qr/.*/,
QUOTEDSTRING => $RE{quoted},
# Networking
MAC => qr/(?:%CISCOMAC%|%WINDOWSMAC%|%COMMONMAC%)/,
CISCOMAC => qr/(?:(?:[A-Fa-f0-9]{4}\.){2}[A-Fa-f0-9]{4})/,
WINDOWSMAC => qr/(?:(?:[A-Fa-f0-9]{2}-){5}[A-Fa-f0-9]{2})/,
COMMONMAC => qr/(?:(?:[A-Fa-f0-9]{2}:){5}[A-Fa-f0-9]{2})/,
IP => $RE{net}{IPv4},
# $RE{net}{domain} fails to match domains starting with a number.
HOSTNAME => qr/(?:[0-9A-Za-z](?:(?:[-A-Za-z0-9]){0,61}[A-Za-z0-9])?(?:\.[A-Za-z](?:(?:[-A-Za-z0-9]){0,61}[A-Za-z0-9])?)*)/,
HOST => "%HOSTNAME%",
IPORHOST => "(?:%IP%|%HOSTNAME%)",
# paths
UNIXPATH => qr/(?<![\w\/])(?:\/[^\/\s?*]*)+/,
WINPATH => qr/(?:\\[^\\?*]*)+/,
URIPROTO => qr/[A-z]+/,
URIHOST => qr/%IPORHOST%(?:%PORT%)?/,
URIPATH => qr/(?:\/[^\/\s?]*)+/,
URIPARAM => qr/\?(?:[A-z0-9]+(?:=(?:[^&]*))?(?:&(?:[A-z0-9]+(?:=(?:[^&]*))?)?)*)?/,
#URL => qr@%URIPROTO%://(?:%URIHOST%)?(?:%UNIXPATH%)(?:%URIPARAM%)??@,
URI => qr@%URIPROTO%://(?:%URIHOST%)?(?:%URIPATH%)(?:%URIPARAM%)?@,
#URI => qr/[A-z]+:\/\/(?:%IPORHOST%(?::[0-9]+)?)?(?:\/[^\/\s]*)*(?:\?[^ ]*)?/,
# Months: January, Feb, 3, 03, 12, December
MONTH => qr/\b(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\b/,
MONTNUM => qr/\b(?:0?[0-9]|1[0-2])\b/,
MONTHDAY => qr/(?:(?:3[01]|[0-2]?[0-9]))/,
# Days: Monday, Tue, Thu, 0 (Sunday?), 6 (Saturday?)
DAY => qr/(?:Mon(?:day)?|Tue(?:sday)?|Wed(?:nesday)?|Thu(?:rsday)?|Fri(?:day)?|Sat(?:urday)?|Sun(?:day)?)/,
# Years?
YEAR => qr/%INT%/,
# Time: HH:MM:SS
TIME => qr/\d{2}:\d{2}(?::\d{2})?/,
# Syslog Dates: Month Day HH:MM:SS
SYSLOGDATE => "%MONTH% +%MONTHDAY% %TIME%",
PROG => "%WORD%",
PID => "%INT%",
SYSLOGPROG => qr/(?:[A-z][\w-]+(?:\/[\w-]+)?)(?:\[%PID%\])?/,
HTTPDATE => qr,%MONTHDAY%/%MONTH%/%YEAR%:%TIME% %INT:ZONE%,,
# Shortcuts
QS => "%QUOTEDSTRING%",
# Log formats
SYSLOGBASE => "%SYSLOGDATE% %HOSTNAME% %SYSLOGPROG%:",
APACHELOG => "%IPORHOST% %USER:IDENT% %USER:AUTH% \\[%HTTPDATE%\\] %QS:URL% %NUMBER:RESPONSE% %NUMBER:BYTES% %QS:REFERRER% %QS:AGENT%",
);
# Dispatch table for filter chains.
# shnq - escaping for use in non-quoted shell context
# shdq - escaping for use in double-quoted shell strings
# e[XYZ] - generic escaping, will escape the characters inside the brackets.
# stripquotes - strip leading and trailing quotes.
# parsedate - convert time values
# uid2user - uid lookups for usernames
our $filters = {
qr/shnq/ => sub { s/([`^()&{}[\]\$*?!|;'"\\])/\\$1/g; $_ },
qr/shdq/ => sub { s/([\\`\$"])/\\$1/g; $_ },
qr/e\[((?:(?:\\])|(?:[^\]]))*)\]/ => sub { s/([$^N])/\\$1/g; $_ },
#qr/e(.)((??{"([^$1]+)"}))$1/ => sub { print STDERR " foo '$1'\n"; return 1 },
qr/stripquotes/ => sub { s/^(['"])//; s/$1$//; return $_ },
qr/parsedate/ => sub { str2time($_) },
qr/strftime\(($RE{quoted})\)/ => sub {
my $m = substr($^N, 1, -1);
$m =~ s/\&/%/g;
POSIX::strftime($m, localtime($_))
},
qr/uid2user/ => sub { ($_) = getpwuid($_); $_ },
qr/urlescape/ => sub { uri_escape($_) },
qr/ip2host/ => sub { my ($host) = gethostbyaddr(inet_aton($_), AF_INET); $host || $_ },
qr/httpfilter/ => sub { s/^\S+ (\S+) \S+$/$1/; $_ },
};
our $opts = {};
getopt('Ffdbm:r:P:', $opts);
# Start the hack.
regexhack::hackinit();
if (exists($opts->{"P"})) {
print pattern2regex($opts->{"P"}) . "\n";
finish_and_exit(0);
}
if (exists($opts->{"d"})) {
# default to debuglevel 1 if -d has no arguments.
$debuglevel = $opts->{"d"} || 1;
debug(1, "Debug level: $debuglevel");
}
if (exists($opts->{"b"})) {
print STDERR "Going to background...\n";
exit if (fork() != 0); # Kill the parent
# Break away from the terminal, if any.
die "Cannot detach from terminal (setsid failed)" unless POSIX::setsid();
#chdir("/");
umask(0);
# Redirect stdin/stdout/stderr to /dev/null
map { close($_) } (\*STDIN,\*STDOUT,\*STDERR);
open(STDIN, "+>/dev/null");
open(STDOUT, "+>&STDIN");
open(STDERR, "+>&STDIN");
# Change ps output to something reasonable?
#$0 = "grok " . join(" ",@ARGV);
}
if (exists($opts->{"F"})) {
require 'grok_patfind.pl';
finish_and_exit();
}
# Syslog to stderr, too, if we aren't going to the background.
my $logopts = LOG_PID;
openlog("grok", $logopts, LOG_USER);
my $config = readconfig($opts->{"f"});
#print Dumper($config);
unless (defined($config)) {
debug(0, "Syntax error in config?");
finish_and_exit(1);
}
setup($config);
my $state;
my $storage; # Match-level storage by File/Key
my $select = IO::Select->new();
my $TAIL = "tail -0f";
foreach (keys(%$config)) {
next if ($_ eq "patterns");
my $s = Symbol::gensym;
my $command = $_;
# Run 'file' types as 'tail -0 -f [file]'
$state->{"orig"}->{$s} = $_;
$command = "$TAIL '$_'" if ($config->{$_}->{"type"} eq 'file');
debug(2, "Startup: Running $command");
open($s, "$command |");
$select->add($s);
$state->{"map"}->{$s} = $_;
(undef, $state->{"inode"}->{$s}) = stat($state->{"orig"}->{$s});
}
# Loop for data for as long as our love shall last...
while($select->count()) {
my @ready = $select->can_read(60);
map(readlog($_), @ready);
}
debug(1, "Nothing left to do (all files/execs ended) exiting");
finish_and_exit(0);
sub finish_and_exit {
my $ret = shift || 0;
regexhack::hackteardown();
exit($ret);
}
sub debug {
my ($level, $msg) = @_;
$msg .= "\n" if (substr($msg,-1) ne "\n");
if ($level <= $debuglevel) {
printf STDERR ("%s: [debug%d] %s", $0, $level, $msg);
syslog(LOG_INFO, "%s", $msg);
}
}
sub readlog {
my $logh = shift;
my $buffer = $state->{"buffers"}->{$logh} || "";
my $bytes = sysread($logh, $buffer, 1024, length($buffer));
if ($bytes == 0) {
debug(1,"finished (possibly unexpected eof?): " . $state->{"map"}->{$logh});
$select->remove($logh);
close($logh);
}
while ($buffer =~ s!(.*)\n!!) {
#print $state->{"map"}->{$logh} . "> $1\n";
handle($state->{"map"}->{$logh},$1);
}
$state->{"buffers"}->{$logh} = $buffer;
}
sub handle {
my ($log,$line,$syslogmode) = @_;
chomp($line);
my $matched = 0;
#print $log . "> " . $line;
$storage->{$log} = {} if (!exists($storage->{$log}));
my $last = { line => $line };
#debug(0, "Syslog: ($log) ".$config->{$log}->{"syslog"});
if ($config->{$log}->{"syslog"}) {
# Syslog repetition message looks like this:
# last message repeated N times
if ($line =~ m/last message repeated (\d+) times$/) {
my $c = $1;
my $lastline = $state->{"last"}->{$log};
if (defined($lastline)) {
handle($log, $lastline, 1) while ($c-- > 0);
} else {
debug(1, "Cannot repeat 'last' line $c times becuase I haven't seen the previous line. ($log)");
}
}
}
my $patterns = $PATTERNS->{$log};
foreach my $pattern (keys(%{$patterns})) {
# "fix" the line, turn it into a regex.
my $regex = pattern2regex($pattern);
# The regex match will store keys in this hash
# XXX: This used to work with 'my' instead of 'our' - I'm not sure why.
our %values = ();
#our %predicates = ();
debug(5, "Line: $line");
debug(5, "Regex: $regex");
debug(5, "Is Match: " . ($line =~ m/$regex/));
if ($line =~ m/($regex)/) {
#debug(5, "Predicates for: " . keys(%predicates));
#next unless check_predicates(\%predicates, \%values);
$matched++;
# Set some other magic entries to %values
$values{'=FILE'} = $log;
$values{'=LINE'} = $line;
$values{'=MATCH'} = $1;
# Handle if we're over the threshold
my $pat = $PATTERNS->{$log}->{$pattern};
my $fileconf = $config->{$log}->{"types"}->{$pat};
my $keyname;
$keyname = $pat . "::" . meta2string($fileconf->{"key"}, \%values) if (exists($fileconf->{"key"}));
$keyname ||= join (",", $pat, map { "$_=>$values{$_}" } keys(%values));
debug(3, "UniqueKey: $keyname");
$state->{$keyname}->{"time"} ||= time();
$state->{$keyname}->{"count"}++;
if ($state->{$keyname}->{"count"} >= $fileconf->{"threshold"}) {
if ($fileconf->{"interval"} == 0
|| ((time() - $state->{$keyname}->{"time"}) <= $fileconf->{"interval"})) {
debug(2, "Limits (threshold/interval) exceeded for rulekey: $keyname");
react($fileconf, \%values, $log, $keyname);
} else {
$state->{$keyname}->{"time"} = time();
}
$state->{$keyname}->{"count"} = 0;
}
$last->{"values"} = \%values;
$last->{"handler"} = $PATTERNS->{$log}->{$pattern};
}
}
# If we haven't matched this line yet, call the 'unmatched' handler?
if ($matched == 0) {
my $fileconf = $config->{$log}->{"unmatched"};
react($fileconf, { "=LINE" => $line }, $log, "unmatched") if (defined($fileconf));
}
$state->{"last"}->{$log} = $last unless ($syslogmode);
}
sub pattern2regex {
my $pattern = shift;
my $regex = $pattern;
my $orig = $pattern;
my $predicate_regex = qr@
(
(?:(?:[<>]=?|==)[^%]+)
|
(?:~$RE{delimited}{-delim=>'/'})
)(?{ $predicate = $^N })
@x;
my $count;
do {
my $rounds = 0;
$count = 0;
# Replace %KEY:SUBNAME% with a regex and store the capture as KEY:SUBNAME
# The :SUBNAME specification is optional,
# ie; %IP% is valid, as is %IP:FOO%
for my $key (keys(%MATCH)) {
my $predicate = "";
my $re = qr,%
(
(?:$key) # Pattern name
(?::\w+)? # Pattern subname
)
(?:$predicate_regex)? # Post-match predicate
%
,x;
$count += $regex =~ s@$re@($MATCH{$key})(??{ handle_capture('$1', \\\%values, '$predicate') })@g;
}
$rounds++;
if ($rounds > 20) {
debug(0, "Deep recursion translating '$orig'");
last;
}
} while ($count > 0);
return $regex;
}
sub meta2string {
my $meta = shift;
my $values = shift;
my $orig = $meta;
my $rounds = 0;
my $count;
do {
$count = 0;
#map { $count += $meta =~ s/%($_)%/$values->{$_}/g; } keys(%MATCH);
# XXX: This will be undef if the key in $values is null, should that ever happen?
map {
my $regex = qr/%(\Q$_\E(?::\w+)?)(?:\|((?:[^,|%]+)(?:[,|][^,|%]+)*))?%/;
while ($meta =~ m/$regex/) {
$count++;
my $data = filter($values, $1, $2);
$meta =~ s/$regex/$data/;
}
} (keys(%MATCH), '=LINE', '=MATCH', '=FILE');
$rounds++;
if ($rounds > 20) {
debug(0, "Deep recursion translating '$orig'");
last;
}
} while ($count > 0);
return $meta;
}
# XXX: There may be some performance issues in this function
sub filter {
my ($values, $key, $opts) = @_;
my $ret = $values->{$key};
$opts = "" unless (defined($opts));
# Wrapper function to call a dispatch function and set $_
local *w = sub {
debug(0, "No filter matches $_[0]") and return unless defined($filters->{$_[0]});
local $_ = $_[1];
$_ = $filters->{$_[0]}->($_[1]);
return $_;
};
# Apply filters in sequence until we run out of filters to apply.
my $re = join("|", map { qr/(?:$_)/ } keys(%$filters));
while ($opts =~ s/^$re//) {
my ($key) = grep { $& =~ m/$_/ } keys(%$filters);
$ret = w($key, $ret);
last unless $opts =~ s/^\|//;
}
debug(2, "Remaining filters (should be empty): '$opts'\n");
return $ret;
}
sub readconfig {
my $cfname = shift || "grok.conf";
debug(2, "Reading from $cfname");
my $grammar = << 'GRAMMAR';
config: <rulevar: local $config = { }>
config: (file | filelist | filecmd | pattern | filter | ignorable )(s) /\Z/ { $return = $config }
ignorable: (comment | blankline)(s)
comment: /#[^\n]*/
blankline: /^\s*\n/
#error: /^[^\Z\n]/ { main::debug(0, "Error on line: $thisline ? (missing a semicolon perhaps?)"); }
string: /((?:\")(?:[^\\\"]*(?:\\.[^\\\"]*)*)(?:\")|(?:\')(?:[^\\\']*(?:\\.[^\\\']*)*)(?:\')|(?:\`)(?:[^\\\`]*(?:\\.[^\\\`]*)*)(?:\`))/ { $return = substr($item[1],1,-1) }
#string: <perl_quotelike> { $return = $item[1]->[2] }
number: /\d+/
word: /\w+/
perlblock: <perl_codeblock>
pattern: <rulevar: local $pattern = { }>
pattern: 'patterns' '{' (pattern_entry)(s) '}' ';' { main::addpatterns($pattern) }
pattern_entry: pattern_name '=' pattern_string ';' { $pattern->{$item[1]} = $item[3] }
pattern_name: word
pattern_string: string
filter: <rulevar: local $filter = { }>
filter: 'filters' '{' (filter_entry | ignorable)(s?) '}' ';' { main::addfilters($filter) }
filter_entry: filter_name '=' filter_string ';' { $filter->{$item[1]} = $item[3] }
filter_name: <perl_quotelike> { $return = $item[1]->[2] }
filter_string: perlblock
file: <rulevar: local $file = { }>
file: filespec string '{' file_entry(?) '}' ';' { $config->{$item[2]} = $file }
filespec: ('file' | 'exec') { $file->{'type'} = $item[1] }
file_entry: (file_entry_line | ignorable)(s)
file_entry_line: (syslog | type | unmatched) ';'
filecmd: <rulevar: local $cmd>
filecmd: <rulevar: local @filelist>
filecmd: 'filecmd' /" */ filecmdstr
filecmdstr: /.*(?=\s*")+/
{
local $/ = "\n";
open($cmd, $item[1] ." |");
push(@filelist, $_) while <$cmd>;
chomp(@filelist);
$text = "filelist \"" .join(',', @filelist) .$text;
}
filelist: <rulevar: local @files>
filelist: <rulevar: local $file = { }>
filelist: ('filelist'|'catlist') string '{' file_entry(?) '}' ';'
{
for (split(/\s*,\s*/, $item[2])) {
for (sort(glob($_))) {
if ($item[1] eq 'filelist') {
$file->{'type'} = "file";
} else {
$file->{'type'} = "exec";
$_ = "cat $_";
}
$config->{$_} = $file;
}
}
}
syslog: 'syslog' '=' number { $file->{"syslog"} = $item[3] }
type: <rulevar: local $type = { }>
type: 'type' typename '{' type_entry '}' { $file->{"types"}->{$item[2]} = $type }
typename: string
type_entry: (((match | threshold | interval | reaction | key | match_syslog | syslog_prog | syslog_host | shell) ';') | ignorable)(s)
match: 'match' '=' string { push(@{$type->{"matches"}},$item[3]) }
threshold: 'threshold' '=' number { $type->{"threshold"} = $item[3] }
interval: 'interval' '=' number { $type->{"interval"} = $item[3] }
key: 'key' '=' string { $type->{"key"} = $item[3] }
shell: 'shell' '=' string { $type->{"shell"} = $item[3] }
reaction: 'reaction' '=' reactionspec { push(@{$type->{"reactions"}}, $item[3]) }
# Let users change the reaction prog.
#reaction_prog: 'reaction_prog' '='
reactionspec: string { $return = { type => "command", string => $item[1] } }
| perlblock {
my $func = 'package main; $func = sub ' . $item[1];
$func =~ s/{/{ my (\$d,\$v,\$s) = \@_;/;
eval($func);
print STDERR "PERL SYNTAX ERROR IN CONFIG:\n$@\n" if $@;
$return = { type => "perl", string => $func }
}
match_syslog: 'match_syslog' '=' number { $type->{"match_syslog"} = $item[3] }
syslog_prog: 'syslog_prog' '=' string { $type->{"syslog_prog"} = $item[3] }
syslog_host: 'syslog_host' '=' string { $type->{"syslog_host"} = $item[3] }
unmatched: <rulevar: local $type = { }>
unmatched: 'unmatched' '{' (reaction ';')(s?) '}' { $file->{"unmatched"} = $type }
GRAMMAR
my $parser = Parse::RecDescent->new($grammar);
my $config;
if ($opts->{"m"}) {
$config = generate_config()
} else {
local $/ = undef;
open(CONFIG, $cfname) or die("Unable to open: $cfname");
$config = <CONFIG>;
close(CONFIG);
}
$config = $parser->config($config);
return $config;
}
sub setup {
my $config = shift;
debug(1, "Watching: " . join(", ", keys(%$config)));
foreach my $file (keys(%{$config})) {
my $matches = $config->{$file}->{"types"};
my $matchhash;
foreach my $match (keys(%{$matches})) {
my $m = $matches->{$match};
debug(2, "Match: $match");
foreach my $string (@{$m->{"matches"}}) {
if ($m->{"match_syslog"}) {
my $prog = ($m->{"syslog_prog"}) ? ($m->{"syslog_prog"} . '(?:\[\\d+\])?') : "%SYSLOGPROG%";
my $host = $m->{"syslog_host"} || "%HOST%";
$string = "%SYSLOGDATE% $host $prog: %DATA:GLOB%$string";
}
debug(2, "String: $string");
$matchhash->{$string} = $match;
}
if (scalar(@{$m->{"matches"}}) == 0) {
if ($m->{"match_syslog"}) {
my $prog = ($m->{"syslog_prog"}) ? ($m->{"syslog_prog"} . '(?:\[\\d+\])?') : "%SYSLOGPROG%";
my $host = $m->{"syslog_host"} || "%HOST%";
my $string = "%SYSLOGDATE% $host $prog: %DATA:GLOB%";
debug(2, "String: $string");
$matchhash->{$string} = $match;
}
}
$matches->{$match}->{"interval"} ||= 0;
$matches->{$match}->{"threshold"} ||= 0;
# Normalize perl blocks ?
#map {
#if ($_->{"type"} eq 'perl') {
##$_->{"string"} =~ s/^\s*{\s*(.*)\s*}\s*$/$1/;
##"Perl: " . $_->{"string"} . "\n";
#}
#} @{$matches->{$match}->{"reactions"}};
}
$PATTERNS->{$file} = $matchhash;
}
}
sub addpatterns {
my $p = shift;
foreach my $pattern (keys(%$p)) {
debug(2, "Adding pattern '$pattern'");
$MATCH{$pattern} = $p->{$pattern};
}
return 0;
}
sub addfilters {
my $f = shift;
#print STDERR Dumper($f);
foreach my $filter (keys(%$f)) {
debug(2, "Adding filter '$filter': " . $f->{$filter});
# Sanitize filter:
my $sf = $filter;
#$sf =~ s/^{|}$//g; # Trim leading and closing curly braces.
$sf = qr/$sf/; # Filter names can be regexes.
if (exists($filters->{$sf})) {
debug(1, "Warning - filter '$filter' already exists and you are overriding it.");
}
$filters->{$sf} = sub { eval($f->{$filter}); }
}
return 0;
}
sub react {
my ($fileconf, $values, $log, $keyname) = @_;
foreach (@{$fileconf->{"reactions"}}) {
my ($cmd,$type) = ($_->{"string"}, $_->{"type"});
if ($type eq 'command') {
$cmd = meta2string($cmd, $values);
debug(2, "Executing (from $keyname): $cmd");
system($cmd);
} elsif ($type eq 'perl') {
my ($d, $v, $s);
# XXX: Should this just be $foo ||= {} ?
$storage->{$log}->{$keyname} = {} if (!exists($storage->{$log}->{$keyname}));
$d = $storage->{$log}->{$keyname};
map { $s->{$_} = $fileconf->{$_} } qw(threshold count interval);
$v = $values;
# XXX: Instead of always evalling this, why not create a
# subroutine like so: my $func; eval('$func = sub { $cmd }')
# and always call $func->() instead?
#eval($cmd);
#print $cmd;
$cmd->($d,$v,$s);
warn $@ if $@;
} else {
debug(0, sprintf("Unknown execution type: %s. %s", $type, $cmd));
}
}
}
sub generate_config {
my $match = $opts->{"m"};
my $reaction = $opts->{"r"} || "%=LINE%";
my $files = "-";
$files = join(",",@ARGV) if (@ARGV);
$reaction =~ s/"/\\"/g;
my $conf = qq(
catlist "$files" {
type "all" {
match = "$match";
reaction = { print meta2string("$reaction\n", \$v); };
};
};);
return $conf
}
sub handle_capture {
my ($word, $values, $predicate) = @_;
my $capture = $^N;
$values->{$word} = $capture;
#$predicates->{$word} = $predicate;
if (defined($predicate) && length($predicate) > 0) {
#debug(0, "Predicate test '$predicate' on '$capture'");
my $ret = regexhack::check($capture, $predicate);
return $ret;
}
return ""
}
### begin regexhack package
# Hack around a regex-make-perl-crash bug
package regexhack;
use IO::Handle;
use Socket;
use Regexp::Common;
use constant STOPRE => '(?=.\A)';
my $child_pid = -1;
sub hackinit {
socketpair(CHILD, PARENT, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
or die("$!");
CHILD->autoflush(1);
PARENT->autoflush(1);
if (($child_pid = fork()) == 0) {
child();
print STDERR "Child exited unexpectedly.\n";
exit(1);
}
}
sub hackteardown {
close(PARENT);
close(CHILD);
kill("SIGTERM", $child_pid);
}
sub check {
my ($capture, $predicate) = @_;
$capture ||= "";
if (!$predicate) {
return "";
}
print CHILD "$capture\n";
print CHILD "$predicate\n";
chomp(my $result = <CHILD>);
return $result;
}
sub child {
$SIG{PIPE} = sub { print STDERR "hack process child got sigpipe\n"; exit(1) };
while (1) {
my $word = <PARENT>;
my $subre = <PARENT>;
last if (! ($word && $subre) );
chomp($word);
chomp($subre);
my $ret = "";
if (length($subre) && !check_predicate($word, $subre)) {
$ret = '(?=.\A)';
}
print PARENT "$ret\n"
}
}
sub check_predicate {
my ($value, $test) = @_;
my (%ops) = (
"~" => sub { $_[0] =~ $_[1] },
"==" => sub { $_[0] == $_[1] },
"<" => sub { $_[0] < $_[1] },
">" => sub { $_[0] > $_[1] },
"<=" => sub { $_[0] <= $_[1] },
">=" => sub { $_[0] >= $_[1] },
"lt" => sub { $_[0] lt $_[1] },
"gt" => sub { $_[0] gt $_[1] },
"le" => sub { $_[0] le $_[1] },
"ge" => sub { $_[0] ge $_[1] },
"eq" => sub { $_[0] eq $_[1] },
);
my (%strcmp) = (
"<" => "lt",
">" => "gt",
"<=" => "le",
">=" => "ge",
"==" => "eq",
);
main::debug(3, "Predicate testing: $test on $value");
if ($test =~ m/^([<>]=?|==)(.*)$/) {
my ($op, $val) = ($1, $2);
my ($a, $b) = ($value, $val);
my $retval;
if ($a =~ m/^$RE{num}{real}$/ && $b =~ m/^$RE{num}{real}$/) {
$retval = $ops{$op}->($a,$b);
} else { # Do string comparison
$retval = $ops{$strcmp{$op}}->($a, $b);
}
main::debug(7, "$a $op $b == " . $retval);
if (!$retval) {
main::debug(4, "Predicate '$test' failed.");
return 0;
}
} elsif ($test =~ m/^(~)(.*)$/) {
my ($op, $val) = ($1, $2);
$val =~ s,^/|/$,,g;
$val =~ s,\\/,/,g;
my ($a, $b) = ($value, $val);
my $retval = $ops{$op}->($a, $b);
main::debug(7, "$a $op $b == " . $retval);
if (!$retval) {
main::debug(4, "Predicate '$test' failed.");
return 0;
}
} else {
main::debug(0, "Unknown test: '$test'");
}
return 1;
}
syntax highlighted by Code2HTML, v. 0.9.1