#!/usr/local/bin/perl

##########################################################################
#
#  Author: Stuart Robinson
#  Description: This script automates the onerous chore of doing 
#    find-replaces globally. It also decreases the danger of such an 
#    operation by allowing you to first check what your find-replace will 
#    do (by using the -dryrun mode) and by providing feedback while it's 
#    running and after it is finished. Furthermore, it is CVS-aware, and 
#    will not do a find-replace on any file that is in edit mode. Note, 
#    however, that CVS-awareness can be disabled using the -nocvs flag.
#
#  Copyright (C) 2002 Stuart P. Robinson
#
#  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.
#
##########################################################################

use Carp;
use English (-no_match_vars);
use File::Find;
use Getopt::Long;
use strict;
use vars qw($dryrun $cvs $help $old_string $new_string $backup $verbose
	    $recursive @directories);

# Two lists for storing filenames
my @processed_files = ();
my @checked_out_files = ();

# Foreground color specifications
my %colors = (black      => 30,
	      blue       => 34,
	      cyan       => 36,
	      green      => 32,
	      magenta    => 35,
	      red        => 31,
	      white      => 37,
	      yellow     => 33);

# Deal w/ command-line options
GetOptions('n'   => \$dryrun,
	   'cvs' => \$cvs,
	   'b'   => \$backup,
	   'r'   => \$recursive,
	   'v'   => \$verbose,
	   'h'   => \$help);

if ($help) {
    help();
}

# Deal w/ arguments
if (scalar @ARGV == 1) {
    @directories = @ARGV;
    ($old_string, $new_string) = get_user_input();
} elsif (scalar @ARGV >= 3) {
    ($old_string, $new_string, @directories) = @ARGV;
} else {
    usage();
}

main(\@directories);


#####################################################################
#
# Main subroutine: this is where everything happens
#
#####################################################################

sub main {
    my $rDirlist = shift;

    # recurse through the files in a directory
    find(\&process_file, @$rDirlist);

    print_results() if $verbose;
}

sub get_user_input {
    # Get the to-be-replaced string
    print "Enter the string (as a regular expression) that you wish to replace:\n";
    my $old_string = <STDIN>;
    chomp $old_string;
    
    # Get the replacement string
    print "\nEnter the string (as plain text) that you wish to use as a replacement:\n";
    my $new_string = <STDIN>;
    chomp $new_string;

    return ($old_string, $new_string);
}

sub print_results {
    # See whether any files were processed
    if (@processed_files == 0 && @checked_out_files == 0) {
	print "\nYour string was not found.\n\n";
    } elsif (@processed_files == 0 && @checked_out_files > 0) {
	print "\nThese files contain your string but are checked out in CVS:\n";
	foreach my $item (@checked_out_files) {
	    print "$item\n";
	}
    } elsif (@processed_files > 0 && @checked_out_files == 0) {
	print "\n\nThese files were affected:\n";

	# List processed files
	foreach my $item (@processed_files) {
	    print "  $item\n";
	}
    } elsif (@processed_files > 0 && @checked_out_files > 0) {
	# List processed files
	foreach my $item (@processed_files) {
	    print "  $item\n";
	}
	
	# List passed over checked-out files (if there are any)
	print "\nThese files contain your string but are checked out in CVS:\n";
	foreach my $item (@checked_out_files) {
	    print "  $item\n";
	}
    } else {
	confess "There is a serious problem with $PROGRAM_NAME: the source code will have to be revisited.\n";
    }
}

#####################################################################
#
# Subroutine that processes each file
#
#####################################################################

sub process_file {
    # If not running recursively, nothing gets processed that isn't under the top directory
    if (!$recursive && -d && $File::Find::name ne $File::Find::topdir) {
	$File::Find::prune = 1;
	return 1;
    }

    # Ignore various file types
    if (-d $_ and $_ =~ /CVS/) { $File::Find::prune = 1; return 1 };
    if (-d $_) { return 1 };
    if (-l $_) { return 1 };
    if ($_ =~ /\.class/) { return 1 };
    if ($_ =~ /\.jar$/) { return 1 };
    if ($_ =~ /\.bak/) { return 1 };
    if ($_ =~ /~$/) { return 1 };

    print "\e\[$colors{'red'}mProcessing $File::Find::name\e\[0m\n" if $verbose;
    my $contents = slurp($_);

    if ($cvs) {
	my $user_name = getpwuid $<;
	my $editors = `cvs editors $_`;
	my $cvs_user = (split(/\s+/, $editors))[1];

	# Ignore file if it's being edited by anyone but user
	if (defined $cvs_user && $cvs_user ne "$user_name") {
	    print "\n\e\[$colors{'magenta'}mCan't process $File::Find::name--being edited by $cvs_user.\e\[0m\n" if $verbose;
	    push(@checked_out_files, $File::Find::name);
	    return 1;
	}

	if ($?) {
	    print "$File::Find::name\n";
	    die $editors;
	}
    }

    # Replace it with replacement string passed in as second argument of command line
    if ($verbose) {
	my @lines = split /\n/, $contents;
	my $i = 0;
	for (@lines) {
	    $i++;

	    # Run regex against line for match
	    if (/$old_string/) {
		my $colorbefore = $_;
		my $colorafter   = $_;
		$colorbefore  =~  s/($old_string)/\e\[$colors{'red'}m$1\e\[0m/g;
		$colorafter   =~  s/$old_string/\e\[$colors{'red'}m$new_string\e\[0m/g;
		print "--Line $i--\n";
		print "  \e\[$colors{'blue'}mBefore:\e\[0m $colorbefore\n";
		print "  \e\[$colors{'blue'}mAfter:\e\[0m $colorafter\n\n";
	    }
	}
    }

    # Was the to-be-replaced string found?
    if ($contents =~ /$old_string/gm) {
	# Add file to list of processed files
	push(@processed_files, $File::Find::name);
	
	unless ($dryrun) {
	    # Do replacement
	    $contents =~ s/$old_string/$new_string/gm;
	    
	    # Print new contents to temp
	    open(TEMP, ">$_.$$") or die "$!: $_";
	    print TEMP $contents;
	    close(TEMP) or die $!;
	    
	    # Backup original, replace original with temp
	    if ($backup) {
		rename("$_", "$_.bak") or die "Couldn't suffix .bak to temp file: $OS_ERROR";
	    }
	    rename("$_.$$", "$_") or die "Couldn't replace original w/ temp file: $OS_ERROR";
	}
    }
    
    # Ditch temp file
    if (-e "$_.$$") {
	unlink("$_.$$") or die "Can't remove temp file: $OS_ERROR";
    }
}

sub usage {
    print "Bad usage. Rerun script with the -h flag for more information.\n";
    exit;
}

sub help {
    print "Usage: $PROGRAM_NAME [-n -cvs -h -v -b -r] [<OLD> <NEW>] <FILE(S)|DIR(S)>\n";
    print "Options:\n"; 
    print " -b    Create backup files of all affected files.\n";
    print " -cvs  Pay attention to CVS when doing replacements.\n";
    print " -h    Get this help message.\n";
    print " -n    Don't actually do replacements.\n";
    print " -r    Recursive.\n";
    print " -v    Verbose.\n";
    exit;
}

sub slurp {
    my $file = shift;
    open(FILE, "<$file") or die "Can't open $file for reading: $!";
    my $contents = do { local $/, <FILE> };
    close(FILE) or warn $!;
    return $contents;
}











