#!/usr/bin/perl
# Filename:     vig (vi-grep), emacsg, pineg, etc..
# Author:       David Ljung Madison <DaveSource.com>
# See License:	http://MarginalHacks.com/License/
  my $VERSION=  2.00;
# Description:  Edits/lists files that match a certain pattern
#		The name of the program specifies the editor
#		to use if the environment var $EDITOR isn't set
# Bugs:		Perl 'glob' function doesn't work if any files contain
#		parentheses.  If this happens then glob returns 0!  :(
#		But then again, it's useful to glob inside perl, because
#		sometimes the arg list is too long for the shell but
#		not too long for perl.
use strict;

##################################################
# Setup the variables
##################################################
my $PROGNAME =  $0;
   $PROGNAME =~ s|.*/||;

# Source files (include verilog)
my @SRC_FILES=qw(*.[Cchsv] *.cc *.cpp *.java);

##################################################
# Usage
##################################################
sub usage {
  foreach my $msg (@_) { print "ERROR:  $msg\n"; }

  print STDERR <<USAGE;

Usage:	$PROGNAME [-whistle#1Xq] <pattern> <fileset ...>

  Edits any of the listed files with lines that match <pattern>
  
  -i	Case insensitive search
  -w	Only match words that equal the pattern (word search)
  -l	Just list files, don't edit them
  -1	Edit the first file we find
  -s	Include source code in the fileset (default if no files listed)
  	Source code = '@SRC_FILES'
  -t	Tree - search files in subdirectories as well
  	  Each -t goes an additional level of subdirectories
  -e	Followed by pattern.  Useful for patterns that start with '-'
    	  Also allows for multiple patterns.
    	  Lines match containing any of these patterns.
  -E	Followed by pattern to 'not match.'
    	  Lines match only if they don't contain any of these patterns.
  -X	Try to skip executable binaries
  -#	Followed by a pattern that must match in the first line
  	(mnemonic: #!/bin/sh)
  -h	Show this help
  -q	Quiet mode

Example:  Edit any source files in a tree two subdirectories deep
          that contain a line with 'Dave' but not 'Class' or 'struct'

  $PROGNAME Dave -E Class -E struct -tt

Example:  Find my personal script that contains 'stupid'

  $PROGNAME stupid -iX1 ~/bin/*

Example:  Look at all my scripts that are ksh

  $PROGNAME -X# ksh ~/bin/*

Example:  Find a perl script that uses opendir

  $PROGNAME opendir -# perl ~/bin/*

USAGE

  exit -1;
}

# Returns the next arg (Breaks -ab flags into -a -b)
sub get_arg {
  return undef if ($#ARGV==-1);
  # Is it a flag(s)?
  if ($ARGV[0] =~ /^-(.)(.*)$/) {
    if ($2) {
      $ARGV[0]="-$2";
    } else {
      shift(@ARGV);
    }
    return "-$1";
  }
  return shift(@ARGV);
}

# Add depth to the file arguments given (a -> a */a)
sub add_depth {
  my ($opt,@files) = @_;
  my @ret;

  for(my $i=0; $i<=$opt->{depth}; $i++) {
    my $file;
    foreach $file (@files) {
      push(@ret,"*/"x$i.$file);
    }
  }
  @ret;
}

sub editor {
  my ($opt) = @_;
  my $editor_choice = $PROGNAME;
     $editor_choice =~ s/g$//;

  # Editor setup
  my $editor=$ENV{EDITOR};
  if ($editor && ! -x $editor) {
    $editor=`which $editor`;
    chomp($editor);
  }
  if (!$editor || ! -x $editor) {
    $editor=`which $editor_choice`;
    chomp($editor);
  }
  $editor=0 if (!$editor || ! -x $editor);

  $opt->{editor} = $editor;

  # Is this a vi clone?  (Does it know 'vi' patterns?)
  my $name = $editor;
  $name=~s|.*/||;
  $name = "vim" if $name eq "v";	# I use an vim wrapper named "v"
  # 'joe' is a vi editor, most likely so is anything of the form '*vi*'
  $opt->{does_vi} = ($name =~ /vi/ || $name eq "joe") ? 1 : 0;
  # vim can do multiple patterns:  pat1\|pat2
  $opt->{does_vim} = ($name =~ /vim/) ? 1 : 0;
}

my ($INTERRUPT_OPT,$INTERRUPT_DATA);
sub parse_args {
  my %opt; my $opt = \%opt;
  $INTERRUPT_OPT = $opt;
  my %data; my $data = \%data;
  $INTERRUPT_DATA = $data;

  my (@files,@ePats,@EPats,@bangPats);
  while(defined(my $arg=get_arg())) {
    if ($arg eq "-h") { usage(); }
    if ($arg eq "-\?") { usage(); }
    if ($arg eq "-s") { $opt->{usr_src_files}=1; next; }
    if ($arg eq "-w") { $opt->{word}=1; next; }
    if ($arg eq "-l") { $opt->{list}=1; next; }
    if ($arg eq "-1") { $opt->{just_one}=1; next; }
    if ($arg eq "-i") { $opt->{ignore_case}=1; next; }
    if ($arg eq "-t") { $opt->{depth}++; next; }
    if ($arg eq "-e") { push(@ePats,shift(@ARGV)); next; }
    if ($arg eq "-E") { push(@EPats,shift(@ARGV)); next; }
    if ($arg eq "-#") { push(@bangPats,shift(@ARGV)); next; }
    if ($arg eq "-q") { $opt->{quiet}=1; next; }
    if ($arg eq "-X") { $opt->{skip_exec}=1; next; }
    if ($arg =~ /^-/) { usage("Unknown flag: $arg"); }

    unless (@ePats || @EPats || @bangPats) {
      push(@ePats, $arg);
    } else {
      push(@files, $arg);
    }
  }

  usage("No patterns defined") unless (@ePats || @EPats || @bangPats);

  # Figure out the editor
  editor($opt);

  $opt->{list}=1 unless $opt->{editor};

  # Use *.[Cchs] files if necessary
  push(@files, @SRC_FILES) if $opt->{usr_src_files};
  @files = (@SRC_FILES) unless @files;
  @files = add_depth($opt,@files) if $opt->{depth};
  # Glob if needed (contains '*')
  @files = map { /\*/ ? glob : $_ } @files;

  usage("No files found\n") unless @files;
  $data->{files} = \@files;
  $data->{num_files} = @files;

  # Do we have a vi pattern?
  if ($opt->{does_vi}) {
    # vim can do multiple patterns, vi cannot
    my @vi_pats = $opt->{does_vim} ? @ePats : $ePats[0];
    @vi_pats = $opt->{does_vim} ? @EPats : $EPats[0] unless @vi_pats;

    # Word
    @vi_pats = map { "\\<$_\\>" } @vi_pats if $opt->{word};

    my $vi_pat = join("\\|", @vi_pats);

    # Parens screw everything up in the vi search
    #$vi_pat =~ s/\\[\(\)]//g;

    # Ignore case?
    if ($opt->{does_vim}) {
      $vi_pat = ($opt->{ignore_case} ? '\c' : '\C') . $vi_pat;
    } else {
      $vi_pat =~ s/([A-Za-z])/[\L${1}\E\U${1}\E]/g;	# Ugh.
    }

    $opt->{vi_pattern} = $vi_pat;
  }

  # Quote parens
  @ePats = map { s/\)/\\)/g; s/\(/\\(/g; $_; } @ePats;
  @EPats = map { s/\)/\\)/g; s/\(/\\(/g; $_; } @EPats;
  @bangPats = map { s/\)/\\)/g; s/\(/\\(/g; $_; } @bangPats;

  # -w
  @ePats = map { "\\b$_\\b" } @ePats if $opt->{word};
  @EPats = map { "\\b$_\\b" } @EPats if $opt->{word};
  @bangPats = map { "\\b$_\\b" } @bangPats if $opt->{word};

  # -i
  @ePats = map { "(?i)$_" } @ePats if $opt->{ignore_case};
  @EPats = map { "(?i)$_" } @EPats if $opt->{ignore_case};
  @bangPats = map { "(?i)$_" } @bangPats if $opt->{ignore_case};

  # Put the patterns in the data hash
  $data->{pats}{e} = \@ePats;
  $data->{pats}{E} = \@EPats;
  $data->{pats}{'#'} = \@bangPats;

  ($opt,$data);
}

##################################################
# Interrupts
##################################################
$SIG{'INT'}='interrupt';	# Ctrl-C?
#$SIG{'TERM'}='interrupt';	# Terminate process (kill)
$SIG{'HUP'}='interrupt';	# Ctrl-C?
$SIG{'QUIT'}='interrupt';	# Bye?

# Do a prompt for one character
sub interrupt {

  my ($opt,$data) = ($INTERRUPT_OPT,$INTERRUPT_DATA);

  # Char-by-char mode
  my $ttyname=`tty`;
  system "/bin/stty -icanon -echo min 1 < $ttyname " if (! $?);

  print STDERR "\n";

  while(1) {
    # Prompt
    print STDERR "\n[Q]uit/[S]kip file/[C]ontinue";
    print STDERR "/[E]dit matches" if $data->{match_files} && !$opt->{list};
    print STDERR "/[L]ist matches" if $data->{match_files};
    print STDERR ": ";

    # Read char
    my $ans;
    read(STDIN,$ans,1);
    print STDERR "\n";

    # Handle option
    print STDERR "\n", exit(-1) if ($ans =~ /Q/i);
    $data->{SKIP_FILE}=1, last if ($ans =~ /S/i);
    last if ($ans =~ /C/i);
    edit_matches($opt,$data), last if $ans =~ /E/i && $data->{match_files};
    list_matches($opt,$data) if $ans =~ /L/i && $data->{match_files};
  }

  # line mode
  `tty -s`;
  system "/bin/stty icanon echo < $ttyname " if (! $? );

  # Reprint the current status line
  search_spin($opt,$data);
}

##################################################
# Routines
##################################################
sub check_pats {
  my ($data,$which) = @_;
  foreach my $pat ( @{$data->{pats}{$which}} ) { return 1 if /$pat/; }
  0;
}

# Search the file for the pattern
sub scan_file {
  my ($opt,$data,$file)=@_;

  $data->{SKIP_FILE}=0;		# See interrupt handler

  if ($opt->{skip_exec}) {
    my $file_out=`file -L $file`;
    (undef,$file_out)=split(/:/,$file_out);
    # These are guesses as to what qualifies as an 'executable'
    # What about: archive|stripped|dynamically linked  ?
    return 0 if $file_out =~ /executable|library|object/
      && $file_out !~ /script|text/;
  }

  if (!open(FILE,$file)) {
    print STDERR "\nERROR:  Couldn't open file: $file\n";
    return 0;
  }

  while (<FILE>) {
    last if ($data->{SKIP_FILE});

    # Check first line
    last if ($.==1 && @{$data->{pats}{'#'}} && !check_pats($data,'#'));

    # If the line has any EPats in it then we go to the next line
    next if check_pats($data,'E');

    # If we only have EPats, then this file matches
    unless (@{$data->{pats}{e}}) {
      close(FILE);
      return 1;
    }

    # If the line has any patterns in it we stop now
    # (also look for case alternatives)
    foreach my $pat (@{$data->{pats}{e}}) {
      if (/($pat)/i) {
        if ($opt->{ignore_case} || /$pat/) {
          close(FILE);
          return 1;
        }
        $data->{alt}{$1}=1;
      }
    }
  }
  close(FILE);
  return 0;
}

my @SEARCH_SPIN=('|','/','-','\\');
sub search_spin {
  my ($opt, $data) = @_;
  return if $opt->{quiet};

  # Clear last line
  printf STDERR ""x($opt->{just_one}?62:72) if $data->{num_spins}++;

  printf STDERR "Num files:%7d  ", $data->{matches} unless $opt->{just_one};

  # Spinner
  printf STDERR $SEARCH_SPIN[$data->{num_spins}%4];

  # Scan out/of
  printf STDERR " %7d/%-7d", $data->{scanning}+1, $data->{num_files};

  # Next file
  printf STDERR " %-35.35s", $data->{files}[$data->{scanning}+1];
}

sub list_matches {
  my ($opt,$data) = @_;
  foreach my $f ( @{$data->{match_files}} ) {
    print "$f\n";
  }
}

sub edit_matches {
  my ($opt,$data) = @_;
  print "No editor found - listing files:\n" unless $opt->{editor};
  return list_matches($opt,$data) if $opt->{list} || !$opt->{editor};

  my $cmd = $opt->{editor};
  $cmd .= " '+/$opt->{vi_pattern}'" if $opt->{vi_pattern};
  $cmd .= " @{$data->{match_files}}";
  system("$opt->{editor} '+/$opt->{vi_pattern}' @{$data->{match_files}}");

  # In case we continue (see interrupt handler)
  @{$data->{match_files}}=();
}

##################################################
# Main code
##################################################
sub main {
  my ($opt,$data) = parse_args();

  #########################
  # Check each file
  #########################
  for ($data->{scanning}=0; $data->{scanning}<$data->{num_files}; $data->{scanning}++) {
    my $file = $data->{files}[$data->{scanning}];
    if (!-p $file && scan_file($opt,$data,$file)) {
      push(@{$data->{match_files}},$file);
      $data->{matches}++;
    }
    search_spin($opt, $data);
    last if $opt->{just_one} && $data->{matches};
  }
  print STDERR "\n" unless $opt->{quiet};

  #########################
  # Did we find any?  Call the editor or list the files
  #########################
  edit_matches($opt,$data) if $data->{match_files};

  if (!$data->{matches} && !$opt->{quiet}) {
    print STDERR "No matches found.\n";
    print "Case alternatives:\n  ".join("\n  ",sort keys %{$data->{alt}})."\n"
      if ($data->{alt});
    exit -2;
  }

} main();		# I like C format main()

