#!/usr/bin/perl
#
# retail - regular expression capable tail
#
# This script emulates tail (include -f option), allowing monitoring
# of multiple files at once.  It also allows for an array of regex
# tests to be defined.  Each of these regex tests would have a subroutine
# associated with it.  Should the regex match a line of output, the code
# would be executed with the line as an argument.
#
# The regex array should be structured as such:
#
# @REGEX = [
#	[ qr/regularexpression/opts, sub { do something; print @_; } ],
#	[ qr/regularexpression/opts, sub { do somethingelse; print @_; } ],
#	[ qr/regularexpression/opts, sub { do something; print BOLD GREEN @_; } ],
#	];
#
# This and other hacks can be found at: http://oddgeek.info/
#
# Copyright (c) 2005 Jason A. Dour
#
# This software is provided 'as-is', without any express or implied warranty.
# In no event will the authors be held liable for any damages arising from the
# use of this software.
#
# Permission is granted to anyone to use this software for any purpose,
# including commercial applications, and to alter it and redistribute it
# freely, subject to the following restrictions:
#
#     1. The origin of this software must not be misrepresented; you must not
#     claim that you wrote the original software. If you use this software in a
#     product, an acknowledgment in the product documentation would be
#     appreciated but is not required.
#
#     2. Altered source versions must be plainly marked as such, and must not
#     be misrepresented as being the original software.
#
#     3. This notice may not be removed or altered from any source
#     distribution.
#

#
# Version Information
#
# 1.0   2005.05.31
#
#       Put in a few additional comments.  Cleaned up formatting.  There seem
#       to be a LOT of similar scripts out there now.  Wow.
#
# primordial ooze
#
#       Used off and on over the years to watch logfiles for important messages
#       or when debugging something.  When used with Term::ANSIColor and GNU
#       Screen, it works as a great log-watcher window.  When I first wrote
#       this, I couldn't find other scripts on the net.
#

#
# Necessary modules.
use Getopt::Regex qw(GetOptions);
use Term::ANSIColor qw(:constants);
use File::Tail;

#
# Global Environment or Configuration

#
# Ensure ANSI colors reset after each use.
$Term::ANSIColor::AUTORESET = 1;;

#
# $progname
#	The name of the script.
$progname = $0;
$progname =~ s/.*\///;



#
# usage()
#	Pretty-print the usage for the user.
sub usage {
    print STDERR <<PEND ;
$progname: error: @_
$progname: usage: $progname [-f] [-#] [-s#] [-c configfilename] filename ...
		-f	follow the file, printing output as 
			it is appended to the file

		-#	print last # of lines of the file

		-s#	sleep # of seconds between checking
			for new data
		-c configfilename
			use alternate config file instead of .${progname}rc
PEND

    exit(99);
}



#
# error()
#	Pretty-print all errors and exit with error code.
sub error {
    $exitval = shift();

    print STDERR "$progname: error: @_\n";

    exit($exitval);
} 


#
# optok()
#	Check the command line options given.
sub optok {
    #
    # Check for at least one filename on command line.
    usage("At least one file must be specified.")
        unless ( $#ARGV > -1 );

    #
    # Check to see if there were multiple files requested.
    if ( $#ARGV > 0 ) {
	$Options{'MultiFile'} = 1;
    } else {
	$Options{'MultiFile'} = 0;
    }

    #
    # Number of lines?  Default is 10.
    $Options{'NumLines'} = 10
	unless ( length($Options{'NumLines'}) );

    #
    # Number of seconds to sleep between EOF checks.
    $Options{'Sleep'} = 1
	unless ( length($Options{'Sleep'}) );

    #
    # Check for insanity in Sleep option.
    if ( $Options{'Sleep'} == 0 ) {
	error(99, "Insane sleep value of 0 - set greater than zero.");
    }

    #
    # Check for alternate config file.
    if ( length($Options{'ConfigFile'}) ) {
	error(99,"Do not have access to config file ($Options{'ConfigFile'}).")
	    unless ( -f $Options{'ConfigFile'} && -r $Options{'ConfigFile'} );
	    do $Options{'ConfigFile'} || 
	        error(99,"Error parsing config file ($Options{'ConfigFile'}).");
    } elsif ( ( -f "$ENV{'HOME'}/.retailrc" && -r "$ENV{'HOME'}/.retailrc" ) ) {
	do "$ENV{'HOME'}/.retailrc" ||
	    error(99,"Error parsing config file (~/.retailrc).");
    } 

    return(1);
}



#
# MAIN

#
# Get the command-line options.
GetOptions(
    \@ARGV,
    ['^-([0-9]+)$', sub { $Options{'NumLines'} = $1; },	0],
    ['^-c$', sub {
	if ( length($_[0]) ) { 
	    $Options{'ConfigFile'} = $_[0];
	} else {
	    usage("No config file specified.");
	}
    }, 1],
    ['^-f$',	sub { $Options{'Follow'} = 1; },	0],
    ['^-s$',	sub { 
	if ( length($_[0]) ) {
	    $Options{'Sleep'} = $_[0];
	} else {
	    usage("No sleep interval specified.");
	}
    }, 1],
    ['^-s([0-9]+)$', sub { $Options{'Sleep'} = $1; },	0],
    ['^(-.*)$', sub { usage("Invalid option ($1)."); },	0],
);

usage("Invalid syntax.")
    unless optok();

#
# Don't buffer STDOUT
$| = 1;

#
# For each file specified on the command line, loop once,
# creating File::Tail instances for each.
for ( $loop = 0; $loop <= $#ARGV; $loop++ ) {
    #
    # Open the file for reading.
    $FILES[$loop] = File::Tail->new(
	name => "$ARGV[$loop]",
	maxinterval => $Options{'Sleep'},
	interval => $Options{'Sleep'},
	adjustafter => 1,
	tail => $Options{'NumLines'},
	reset_tail => -1,
    );

    #
    # Assign name to array indexed on filehandles.
    $NAMES{$FILES[$loop]} = "$ARGV[$loop]";
}

#
# Set flag to prevent needless headers.
$lastprt = $FILES[$#ARGV];

#
# Loop once or forever, depending on Follow being specified.
do {
    #
    # Select those files that have data waiting to be read.
    ($dsr, $remtime, @DSRFILES) = File::Tail::select(undef, undef, undef, 60, @FILES);

    #
    # For each of the files from above, loop once to read new data.
    foreach $file ( @DSRFILES ) {
	#
	# Header if necessary.
	if (  $Options{'MultiFile'} && ($lastprt != $file) ) {
	    print BOLD YELLOW "***** $NAMES{$file} *****\n";
	}
	#
	# Loop through each line of input from file.
	line: foreach $line ( $file->read ) {
	    #
	    # Check for regular expression array to be i
	    # used for operating on lines.
	    if ( defined(@REGEX) ) {
		#
		# Apply every regex couplet.
		foreach $regex (@REGEX) {
		    #
		    # If the regex hits, pass $line to the code...
		    if ( $line =~ /(?-xism:$regex->[0])/ ) {
			$code = $regex->[1];
			&$code($line);
			undef($code);
			next line;
		    }
		}
	    }
	    #
	    # If no array or match, print as normal.
	    print $line;
	}
	#
	# Set flag for last file that was printed.
	$lastprt = $file;
    }

    #
    # Destroy per-loop variables.
    undef($dsr); undef($remtime); undef(@DSRFILES);
} while ( $Options{'Follow'} );

#
# We're done...
exit(0);
