#!/usr/bin/perl

# ---------------------------------------------------------------------------------------------------------------------
# Multifile Tail with grep
#
# author: Dimitris Evmorfopoulos
# date:   Jun, 06 1998
# email:  devmorfo@hol.net
#
#
# This program was created to help monitor multiple log files for multiple events and present those on a screen. It
# helps keep down the number of tails and greps on a server that clutter the process list and make it difficult to 
# find the processes that need to be killed. Also being left as a stale process it is easier to track even if it monitors 
# many files for many events.
#
# In general mftg was created to be operated with configuration files. You can create any set of configuration files for 
# the different types of jobs you need to do, and run the script with the needed configuration.
# 
# It was extended to include command line arguments for files and search items for arbitray usage, but then on the command 
# line there is no support for labeling and coloring of the log lines.
# 
# See mftg.pl -h for some help on how to create configuration files and how to use the script.
#
# Enjoy.
#
# ---------------------------------------------------------------------------------------------------------------------

use strict;
use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK SEEK_END);

# ---------------------------------------------------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------------------------------------------------

my %files = ();

my $show_labels = 1;
my $show_files = 1;
my $show_line_color = 0;
my $report_only = 0;
my $conf_file = $ENV{HOME} . '/.mftg';

my $falign = 0;
my $lalign = 0;

my $rin_bits = '';
my $err_bits = '';

# ---------------------------------------------------------------------------------------------------------------------
# Add file to select read bits vector
# ---------------------------------------------------------------------------------------------------------------------

sub add_file {
  my ($fname) = @_;
  my $fno = -1;
  my $local_bits = '';

  if (length($fname) > $falign) { $falign = length($fname); }

  if (not exists $files{$fname}) {
    open $files{$fname}{HANDLE}, $fname || die "Cannot open file $fname\n\n";
    $fno = fileno($files{$fname}{HANDLE});
    if (!$report_only) {
      seek($files{$fname}{HANDLE}, 0, SEEK_END);

      my $flags;
      fcntl($files{$fname}{HANDLE}, F_GETFL, $flags);
      $flags |= O_NONBLOCK;
      fcntl($files{$fname}{HANDLE}, F_SETFL, $flags);
    }

    vec($rin_bits, $fno, 1) = 1;
    vec($local_bits, $fno, 1) = 1;
    $err_bits = $rin_bits;

    $files{$fname}{BITS} = $local_bits;
    $files{$fname}{LAST} = '';
    $files{$fname}{TEST} = {};
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Color align label
# ---------------------------------------------------------------------------------------------------------------------

sub color_align_label {
  my ($align, $color, $label) = @_;
  my $add = '';

  while (length($label) + length($add) < $align) {
    $add .= ' ';
  }

  return $color . $label . "\e[m" . $add;
}

# ---------------------------------------------------------------------------------------------------------------------
# Test line for display
# ---------------------------------------------------------------------------------------------------------------------

sub test_line {
  my ($fname, $line) = @_;

  for my $test (keys %{ $files{$fname}{TEST} }) {
    if (($test eq '') || ($line =~ $test)) {
      my $label = $files{$fname}{TEST}{$test}{LABEL};
      my $color = $files{$fname}{TEST}{$test}{COLOR};
      if ($show_files) {
	printf("%-*s: ", $falign, $fname);
      }
      if ($show_labels) {
	printf("%s - ", color_align_label($lalign, $color, $label));
      }
      if ($show_line_color) {
	printf("%s%s\e[m\n", $color, $line);
      } else {
	printf("%s\n", $line);
      }
    }
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Wait for file input
# ---------------------------------------------------------------------------------------------------------------------

sub wait_files {
  my $rout_bits = '';
  my $eout_bits = '';

  while (1) {
    my ($found) = select($rout_bits = $rin_bits, undef, $eout_bits = $err_bits, undef);
    next unless $found;

    if ($found == -1) {
      die "Failed on select with $!\n\n";
    }

    for my $f (keys %files) {
      my $fh = $files{$f}{BITS};
      if (select($fh, undef, undef, 0)) { # we need to read from the file
	my $buffer = '';
	my $bread = sysread($files{$f}{HANDLE}, $buffer, 8192);
	if (not defined $bread) {
	  die "Error reading from file " . $f . ". Exiting\n\n";
	}
	$buffer = $files{$f}{LAST} . $buffer;
	$files{$f}{LAST} = '';
	if ($buffer ne '') {
	  if ($buffer =~ /^(.*?)\n(.*)$/o) {
	    my $tl = $1;
	    my $rest = $2;

	    test_line($f, $tl);

	    $files{$f}{LAST} = $rest;
	  }
	}
      }
      if (select(undef, undef, $fh, 0)) { # we have an error from the file
	die "Error on file " . $f . ". Exiting\n\n";
      }
    }
  }
    
}

# ---------------------------------------------------------------------------------------------------------------------
# Report on files 
# ---------------------------------------------------------------------------------------------------------------------

sub report_files {
  for my $f (keys %files) {
    my $fh = $files{$f}{HANDLE};
    while (my $l = <$fh>) {
      $l =~ s/\n//gc;
      $l =~ s/\r//gc;

      test_line($f, $l);

    }
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Make color for test line
# ---------------------------------------------------------------------------------------------------------------------

sub make_color {
  my ($line, $file, $fg, $bg, $st) = @_;
  if (not defined $fg) { $fg = '-'; }
  if (not defined $bg) { $bg = '-'; }
  if (not defined $st) { $st = '-'; }

  $fg =~ s/^\s+//gco;
  $fg =~ s/\s+$//gco;
  $bg =~ s/^\s+//gco;
  $bg =~ s/\s+$//gco;
  $st =~ s/^\s+//gco;
  $st =~ s/\s+$//gco;
  
  my @fgs = qw(30 31 32 33 34 35 36 37);
  my @bgs = qw(40 41 42 43 44 45 46 47);

  my @fg_names = qw(black red green orange blue magenta cyan light-gray dark-gray light-red light-green yellow light-blue light-magenta light-cyan white);
  my @bg_names = qw(black red green orange blue magenta cyan light-gray);

  my $res = "\e[";
  if ($fg ne '-') {
    if ($fg !~ /^\d+$/) {
      for (my $i = 0; $i < @fg_names; $i++) {
	my $cn = $fg_names[$i];
	if (uc($fg) eq uc($cn)) {
	  $fg = $i;
	  last;
	}
      }
      if ($fg !~ /^\d+$/) {
        print "Invalid color name ($fg) given for foreground on line $line on file $file\n\n";
        usage();
      }
    }
    if ($fg > 7) {
      $fg -= 8;
      $res .= "1;";
      $res .= $fgs[$fg] . ";";
    } else {
      $res .= "22;";
      $res .= $fgs[$fg] . ";";
    } 
  }
  
  if ($bg ne '-') {
    if ($bg !~ /^\d+$/) {
      for (my $i = 0; $i < @bg_names; $i++) {
	my $cn = $bg_names[$i];
	if (uc($bg) eq uc($cn)) {
	  $bg = $i;
	  last;
	}
      }
      if ($bg !~ /^\d+$/) {
        print "Invalid color name ($bg) given for background on line $line on file $file\n\n";
        usage();
      }
    }
    if ($bg > 7) {
      $bg -= 8;
      $res .= $bgs[$bg] . ";";
    } else {
      $res .= $bgs[$bg] . ";";
    }
  }
  
  if ($st ne '-') {
     if (uc($st) eq 'U') {
       $res .= "04;";
     } elsif (uc($st) eq 'B') {
       $res .= "05;";
     }
  }
  
  $res =~ s/;$//gc;
  $res .= "m";

  return $res;
}

# ---------------------------------------------------------------------------------------------------------------------
# Parse csv
# ---------------------------------------------------------------------------------------------------------------------

sub parse_csv {
  my ($ref, $sep) = @_;

  if (not defined $sep) { $sep = ';'; }
    my $match = '"([^\"\\\\]*(?:\\\\.[^\"\\\\]*)*)"' . $sep . '?  |  ([^' . $sep . ']+)' . $sep . '?  | '. $sep;

  my @new = ();
  push(@new, $+) while $ref =~ m/$match/gx;
    push(@new, undef) if substr($ref, -1,1) eq $sep;
  return @new;
}

# ---------------------------------------------------------------------------------------------------------------------
# Parse .mftg file
# ---------------------------------------------------------------------------------------------------------------------

sub parse_mftg {
  my $cname = $conf_file;
  if (-e $cname) {
    my $cfile = '';
    my $line = 0;
    open F, $cname || die "Cannot open configuration file $cname\n\n";
    while (my $l = <F>) {
      $l =~ s/\n//gc;
      $l =~ s/\r//gc;
      $l =~ s/^\s+//gc;
      $l =~ s/\s+$//gc;
      $l =~ s/\s+,/,/gc;
      $l =~ s/,\s+/,/gc;
      $l =~ s/^(.*?)\#.*$/$1/gc;
      $line++;
      if ($l ne '') {
	if ($cfile ne '') {
	  if ($l =~ /^\}$/) {
	    $cfile = '';
	  } else {
	    my ($label, $test, $fore, $back, $style) = parse_csv($l, ",");
	    $files{$cfile}{TEST}{$test}{COLOR} = make_color($line, $cname, $fore, $back, $style);
	    $files{$cfile}{TEST}{$test}{LABEL} = " $label ";
            if (length(" $label ") > $lalign) { $lalign = length(" $label "); }
	    if (($show_labels) && (!$show_files) && (length($label) == 0)) {
	      die "Cannot specify -L when labels are missing.\nDefine all labels first in the configuration file or avoid using -L switch\n\n";
	    }
	  }
	} else {
	  if ($l =~ /^(.*?)\s*\{$/) {
	    $cfile = $1;
	    if (-e $cfile) {
	      add_file($cfile);
	    } else {
	      die "File $cfile found on line $line of $cname is not accessible\n\n";
	    }
	  } else {
	    die "Invalid syntax on line $line of $cname\n\n";
	  }
	}
      }
    }
    close F;
  } else {
    usage();
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Test command line for switches
# ---------------------------------------------------------------------------------------------------------------------

sub test_options {
  while (1) {
    my $cmd = shift @ARGV;
    if (not defined $cmd) {
      return 0;
    }
    if ($cmd =~ /^\-/) {
      if (($cmd eq '-h') || ($cmd eq '--help')) {
	usage();
      } elsif (($cmd eq '-f') || ($cmd eq '--file')) {
	$conf_file = shift @ARGV;
      } elsif (($cmd eq '-r') || ($cmd eq '--report')) {
	$report_only = 1;
      } elsif (($cmd eq '-L') || ($cmd eq '--labels-remove')) {
	$show_labels = 0;
	$show_line_color = 1;
      } elsif (($cmd eq '-F') || ($cmd eq '--files-remove')) {
	$show_files = 0;;
      } elsif (($cmd eq '-C') || ($cmd eq '--force-line-color')) {
	$show_line_color = 1;
      }
    } else {
      unshift @ARGV, $cmd;
      return 1;
    }
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Parse command line arguments
# ---------------------------------------------------------------------------------------------------------------------

sub parse_cmd {
  my $i = 0;
  if (@ARGV % 2 != 0) {
    print "Invalid number of arguments\n\n";
    usage();
  }
  while ($i < @ARGV) {
    my $fname = $ARGV[$i++];
    if (-e $fname) {
      add_file($fname);
      $files{$fname}{TEST}{$ARGV[$i]}{COLOR} = '';
      $files{$fname}{TEST}{$ARGV[$i]}{LABEL} = '';
      $i++;
    } else {
      die "Cannot access file $fname\n\n";
    }
  }
}

# ---------------------------------------------------------------------------------------------------------------------
# Usage
# ---------------------------------------------------------------------------------------------------------------------

sub usage {
  print <<EOFUSAGE;
usage is: $0 -h -r -f <conf_file> <filename search-string> ...

  -h, --help             : This help message
  -f, --file <conf_file> : Read the configuration from this file
  -r, --report           : Just produce a report on the files and serach strings
  -L, --labels-remove    : Dont show labels.
  -F, --files-remove     : Dont show file names.
  -C, --no-line-color    : Dont color log line. Label will be colored if present

  You can specify any number of filename - search-string tuples to monitor files with. 
  Specifying a file more than once is acceptable and does not increase the number of 
  opened files for the program.

  Alternatively you can have a .mftg file in your home directory that will be used when 
  you do not specify any command line arguments. The format of this file is as follows:

  filename {
    "label to show","string to monitor"
    "label to show","another string to monitor"
  } 

  In both command line and .mftg file the serach strings can be an empty string "" if 
  you just wish to simply tail those files without monitoring for a specific string. 
  
  Monitoring strings can be perl regular expressions that will be matched against 
  each line read from the file, so a search string of "This\s*is\s+example number[0-9]+" 
  is a valid serach string.
  
  In .mftg yopu can also specify the color of the matrched line to make the program 
  output more readable when monitoring multiple files. The colors are specified as 
  foreground background style (space separated) and can be any number between 0 and 15 
  for either color and U anf B for underline and blink in style. You can ommit all color 
  specification or if you place then to ommit one you can type - in its place

  An example of this is as follows:

  filename {
    "label to show","string to monitor",15,-,- 
    "","\\Wtest:",15,4,-
  }

  which results in white foreground and default background for the first one and white 
  foreground with read background for the second one.

  Finally the color values can also be given as color names. Valid color names for 
  foreground are the foillowing:

  black red green orange blue magenta cyan light-gray 
  dark-gray light-red light-green yellow light-blue light-magenta light-cyan white

  and for background the following:

  black red green orange blue magenta cyan light-gray 

EOFUSAGE
  exit(0);
}

# ---------------------------------------------------------------------------------------------------------------------
# Glorious main
# ---------------------------------------------------------------------------------------------------------------------

if (@ARGV > 0) {
  if (test_options()) {
    parse_cmd();
  } else {
    parse_mftg();
  }
} else {
  parse_mftg();
}

if ($report_only) {
  report_files();
} else {
  wait_files();
}

1;

# ---------------------------------------------------------------------------------------------------------------------
# Thats all for now
# ---------------------------------------------------------------------------------------------------------------------

