#!/usr/bin/perl -w
#
# search - Search a file for text. Like grep, but prints a window.
#          Perl, Unix/Linux/Windows...
#
# 23-Jan-2006, ver 1.20
#
# USAGE:    search [-hinBI][-s num] pattern [files ...]
#
#           -h          # usage help
#           -i          # ignore case
#           -n          # print line numbers
#           -B          # don't print borders
#           -I          # don't invert match background
#           -s num      # number of surrounding lines
#   eg,
#           search fred msg.txt        # find "fred" in the file "msg.txt".
#           search -i fred msg.txt     # case insensitive: "fred", "Fred"...
#           search -n fred msg.txt     # print line numbers.
#           search -s3 fred msg.txt    # print 3 surrounding lines.
#           search fred file1 file2    # find "fred" in multiple files.
#           cmd | search fred          # search for "fred" in command output.
# 
# Search is similiar to grep, however prints the surrounding two lines when
# it finds matches. This is similar to the OpenVMS search command, or GNU's
# grep with the window option. Regular Expressions are supported.
#
# The "pattern" is the search term. If regular expressions are used, they
# are best kept in single forward quotes.
#
# Originally written in several lines, it was too difficult to follow. It
# has now been rewritten in a verbose and conventional Perl style.
#
# SEE ALSO: GNU grep
#
# COPYRIGHT: Copyright (c) 2006 Brendan Gregg.
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#  (http://www.gnu.org/copyleft/gpl.html)
#
# 26-Jun-2003    Brendan Gregg    Created this.
# 23-Jan-2006       "      "      Tweaked style.

use strict;
use Getopt::Long;
Getopt::Long::Configure ("bundling", "no_ignore_case");

#
#  Setup Variables
#
my @Files = ();             # Files to print.
my @Buffer = ();            # Contains lines that may be printed.
my $inv = `tput smso`;      # Inverse ESC sequence.
my $off = `tput rmso`;      # Reset ESC sequence.
my $state = "buffering";    # State of the main routine.
my $WINDOW  = 2;            # Defaults, ...
my $INVERSE = 1;
my $NUMBERS = 0;
my $BORDERS = 1;
my $IGNORECASE = 0;
my $linenum = 0;
my $border  = 0;
my $print   = 0;
my $file    = "";

#
#  Parse Options 
#
my %Options = (
    'noborders'  => sub { $BORDERS = 0; },
    'noinverse'  => sub { $INVERSE = 0; },
    'help'       => sub { usage(); },
    'ignorecase' => \$IGNORECASE,
    'numbers'    => \$NUMBERS,
    'size'       => \$WINDOW,
);
GetOptions (\%Options, 'noborders|B', 'noinverse|I', 'help|h',
                       'ignorecase|i', 'numbers|n', 'size|s=i') or usage();
my $word = shift;
usage() unless defined $word and $word ne "";
while (my $file = shift) {
    push @Files, $file;
}
 
#
#  MAIN 
#
if (@Files > 0) {
    ### search files
    foreach $file (@Files) {
        open IN, $file or die "ERROR: Can't open file, $file: $!\n";
    
        $linenum = 0;
        while (my $line = <IN>) {
            search_line($line);
        }
        close IN;
    }
}
else {
    ### search STDIN
    while (my $line = <STDIN>) {
        search_line($line);
    }
}

# search_line - searches a line for the input word.
#
# This is the main routine to search $line and print results. This is 
# a shortcut to a lump of code rather than a regular subroutine (hense
# the usage of so many globals).
#
# Global flags used are $IGNORECASE, $NUMBERS, $BORDERS, $INVERSE, $WINDOW.
# Global vars used are @Buffer, $state, $print, $linenum, @Files, $file.
#
# This is a stateful subroutine, which can be followed by the $state var.
# This was set as a string for readability (not speed).
#
sub search_line { 
    my $line = shift;
    my $junk;
    
    ### Check for a match
    if ($IGNORECASE) {
        $state = "found" if $line =~ /$word/i;
    }
    else {
        $state = "found" if $line =~ /$word/;
    }

    ### Add line numbers
    if ($NUMBERS) {
        $linenum++;
        $line =~ s/^/$linenum:/;
    }

    ### Add filename
    if (@Files > 1) {
        $line =~ s/^/$file:/;
    }

    ### Found, match found, print lines and buffer
    if ($state eq "found") {
        if ($border && $BORDERS && (- $print > $WINDOW)) { 
            # The third term above solves a bug when sequential
            # windows shouldn't have a border.
            print "-----\n";
            $border = 0;
        }
        if ($INVERSE) {
            if ($IGNORECASE) {
                $line =~ s/($word)/$inv$1$off/ig;
            }
            else {
                $line =~ s/($word)/$inv$1$off/g;
            }
        }
        print @Buffer, $line;
        @Buffer = ();
        $print = $WINDOW;         # print more lines.
        $state = "printing";
    }

    ### Printing, keep printing lines from a previous match
    elsif ($state eq "printing") {
        print $line if $print;
        $print--;            
        if ($print <= 0) {        # no more lines to print.
            $border = 1;          # print border if needed.
            $state = "buffering";
        }
    }

    ### Buffering, buffer lines in case of a future match
    elsif ($state eq "buffering") {
        push @Buffer, $line;
        $junk = shift @Buffer if @Buffer > $WINDOW;
        $print--;                 # counter for a border check.
    }
}

# usage - print the usage message and exit.
#
sub usage {
    print STDERR qq/USAGE: search [-hinBI][-s num] pattern [files ...]

               -h              # usage help
               -i              # ignore case
               -n              # print line numbers
               -B              # don't print borders
               -I              # don't invert match background
               -s num          # number of surrounding lines
       eg,
               search fred msg.txt      # find "fred" in the file "msg.txt".
               search -i fred msg.txt   # case insensitive: "fred", "Fred"...
               search -n fred msg.txt   # print line numbers.
               search -s3 fred msg.txt  # print 3 surrounding lines.
               search fred file1 file2  # find "fred" in multiple files.
               cmd | search fred        # search for "fred" in output.\n/;
    exit 1;
}
