I have a script in Perl that searches for an error that is in a config file, but it prints out any occurrence of the error. I need to match what is in the config file and print out only the last time the error occurred. Any ideas?
In outline:
my $errinfo;
while (<>)
{
$errinfo = "whatever" if (m/the error pattern/);
}
print "error: $errinfo\n" if ($errinfo);
This catches all errors, but doesn't print until the end, when only the last one survives.
Another way to do it:
perl -n -e '$e = $1 if /(REGEX_HERE)/; END{ print $e }' CONFIG_FILE_HERE
What exactly do you need to print? The line containing the error? More context than that?
File::ReadBackwards
can be helpful.
A brute-force approach involves setting up your own pipeline by pointing STDOUT
to tail
. This allows you to print all errors, and then it's up to tail
to worry about only letting the last one out.
You didn't specify, so I assume a legal config line is of the form
Name = some value
Matching that is straightforward:
^
(starting at the beginning of line)\w+
(one or more “word characters”)\s+
(followed by mandatory whitespace)=
(followed by an equals sign)\s+
(more mandatory whitespace).+
(some mandatory value)$
(finishing at the end of the line)
Gluing it together, we get
#! /usr/bin/perl
use warnings;
use strict;
# for demo only
*ARGV = *DATA;
my $pid = open STDOUT, "|-", "tail", "-1" or die "$0: open: $!";
while (<>) {
print unless /^ \w+ \s+ = \s+ .+ $/x;
}
close STDOUT or warn "$0: close: $!";
__DATA__
This = assignment is ok
But := not this
And == definitely not this
Output:
$ ./lasterr And == definitely not this
With regular expressions, when you want the last occurrence of a pattern, place ^.*
at the front of your pattern. For example, to replace the last X in the input with Y, use
$ echo XABCXXXQQQXX | perl -pe 's/^(.*)X/$1Y/' XABCXXXQQQXY
Note that the ^
is redundant because regular-expression quantifiers are greedy, but I like having it there for emphasis.
Applying this technique to your problem, you can search for the last line in your config file that contains an error as in the following program:
#! /usr/bin/perl
use warnings;
use strict;
local $_ = do { local $/; scalar <DATA> };
if (/\A.* ^(?! \w+ \s+ = \s+ [^\r\n]+ $) (.+?)$/smx) {
print $1, "\n";
}
__DATA__
This = assignment is ok
But := not this
And == definitely not this
The syntax of the regular expression is a bit different because $_
contains multiple lines, but the principle is the same. \A
is similar to ^
, but it matches only at the beginning of string to be searched. With the /m
switch (“multi-line”), ^
matches at logical line boundaries.
Up to this point, we know the pattern
/\A.* ^ .../
matches the last line that looks like something. The negative look-ahead assertion (?!...)
looks for a line that is not a legal config line. Ordinarily .
matches any character except newline, but the /s
switch (“single line”) lifts this restriction. Specifying [^\r\n]+
, that is, one or more characters that are neither carriage return nor line feed, does not allow the match to spill into the next line.
Look-around assertions do not capture, so we grab the offending line with (.+?)$
. The reason it's safe to use .
in this context is because we know the current line is bad and the non-greedy quantifier +?
stops matching as soon as it can, which in this case is the end of the current logical line.
All these regular expressions use the /x
switch (“extended mode”) to allow extra whitespace: the aim is to improve readability.
Wow...I was not expecting this much of a response. I should've been more clear in stating this is for log monitoring on a windows box that sends an alert to Nagios. This is actually my first Perl program and all this information has been very helpful. Does anyone know how I can apply this any of the tail answers on a wintel box?