#!/usr/bin/perl
# Filename:     vig (vi-grep), emacsg, etc..
# Author:       David Ljung Madison <davesource.com>
# Description:  Edits files that match a certain pattern
#		The name of the program specifies the editor to use if
#		  $EDITOR isn't set
# Ideas:	Have the ability to do multiple patterns that either:
#		  1) need to be on the same line (and)
#		     [NOPE - this can mostly be done with regexp]
#		  2) either one on a line is a match (or)  [ ADDED ]
#		  3) need to match but can be in different lines (also)
#		  4) Complex combinations/expressions of the above  :)
#		     (Wouldn't that be cool? :)
# 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 (try vig 'blah' ~/txt/Funny/*/*)
#
# PATTERN_IS_LABEL is screwed - it should really use zero-width lookahead,
#   or maybe just word boundaries??  (i.e., (?!regexp) or \w) - see perlre man
#
# CASE_INSENSITIVE should use (?i) to get /i flag inside of expression

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

# Editor setup
$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);

# Is this a vi clone?  (Does it know 'vi' patterns?)
$EDITOR_NAME=$EDITOR;
$EDITOR_NAME=~s|.*/||;
# 'joe' is a vi editor, most likely so is anything of the form '*vi*'
$USE_VI=($EDITOR_NAME =~ /vi/ || $EDITOR_NAME eq "joe");
# vim can do multiple patterns:  pat1\|pat2
$USE_VIM=($EDITOR_NAME =~ /vim/);

# Non-label character
$NON_LABEL="\[^a-zA-Z_-]";
# Source files (include verilog)
$ORIGINAL_SRC_FILES="*.[Cchsv] *.cc *.java";
$SRC_FILES=$ORIGINAL_SRC_FILES;

##################################################
# Usage
##################################################
sub usage {
  foreach $msg (@_) { print "ERROR:  $msg\n"; }
  print "\n";
  print "Usage:\t$PROGNAME [-whistle#1Xq] <pattern> <fileset ...>\n";
  print "\n";
  print "\tEdits any of the listed files with lines that match <pattern>\n";
  print "\n";
  print "\t-i\tCase insensitive search\n";
  print "\t-w\tOnly match labels that equal the pattern (word search)\n";
  print "\t-l\tJust list files, don't edit them\n";
  print "\t-1\tEdit the first file we find\n";
  print "\t-s\tInclude $ORIGINAL_SRC_FILES in the fileset (default if no files listed)\n";
  print "\t-t\tSource tree (also check for $ORIGINAL_SRC_FILES in local subdirectories)\n";
  print "\t\t  Each -t goes an additional level of subdirectories\n";
  print "\t-e\tFollowed by pattern.  Useful for patterns that start with '-'\n";
  print "\t  \t  Can also give multiple patterns.  A line will match if it\n";
  print "\t  \t  contains any of these patterns.\n";
  print "\t-E\tFollowed by pattern to 'not match.'  A line will only match\n";
  print "\t  \t  if it doesn't contain any of these patterns.\n";
  print "\t-X\tTry to skip executable binaries\n";
  print "\t-#\tFollowed by a pattern that must match in the first line\n";
  print "\t\t(mnemonic: #!/bin/sh)\n";
  print "\t-h\tShow this help\n";
  print "\t-q\tQuiet mode\n";
  print "\n";
  print "Example:  Edit any source files in a tree two subdirectories deep\n";
  print "          that contain a line with 'Dave' but not 'Class' or 'struct'\n";
  print "\n";
  print "  $PROGNAME Dave -E Class -E struct -tt\n";
  print "\n";
  print "Example:  Find my personal script that contains 'stupid'\n";
  print "\n";
  print "  $PROGNAME stupid -iX1 ~/bin/*\n";
  print "\n";
  print "Example:  Look at all my scripts that are ksh\n";
  print "\n";
  print "  $PROGNAME -X# ksh ~/bin/*\n";
  print "\n";
  print "Example:  Find a perl script that uses opendir\n";
  print "\n";
  print "  $PROGNAME opendir -# perl ~/bin/*\n";
  print "\n";
  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 ($files)=@_;
  my @files=split(/ /,"$files ");
  my @ret;

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

sub parse_args {
  @patterns=();  @search_patterns=();
  @not_patterns=();  @not_search_patterns=();
  $vi_pattern="";
  $depth=0;
  while(defined($arg=get_arg())) {
    if ($arg eq "-h") { &usage; }
    if ($arg eq "-\?") { &usage; }
    if ($arg eq "-s") { $USE_SRC_FILES=1; next; }
    if ($arg eq "-w") { $PATTERN_IS_LABEL=1; next; }
    if ($arg eq "-l") { $JUST_LIST=1; next; }
    if ($arg eq "-1") { $JUST_ONE=1; next; }
    if ($arg eq "-i") { $CASE_INSENSITIVE=1; next; }
    if ($arg eq "-t") { $depth++; next; }
    if ($arg eq "-e") { push(@patterns,shift(@ARGV)); next; }
    if ($arg eq "-E") { push(@not_patterns,shift(@ARGV)); next; }
    if ($arg eq "-#") { push(@first_line_patterns,shift(@ARGV)); next; }
    if ($arg eq "-q") { $QUIET_MODE=1; next; }
    if ($arg eq "-X") { $SKIP_EXECUTABLES=1; next; }
    if ($arg =~ /^-/) { &usage("Unknown flag: $arg"); }

    if ($#patterns==-1 && $#not_patterns==-1 && $#first_line_patterns==-1) {
      push(@patterns,$arg);
    } else {
      $fileset.=(defined($fileset)?" ":"").$arg;
    }
  }
  usage("No patterns defined")
    if ($#patterns == -1 && $#not_patterns == -1 && $#first_line_patterns==-1);

  $JUST_LIST=1 if (!$EDITOR);

  # Use *.[Cchs] files if necessary
  $fileset.=($fileset?" ":"").$SRC_FILES if ($USE_SRC_FILES);
  $fileset=$SRC_FILES if (!$fileset);
  $fileset=&add_depth($fileset) if ($depth);

  # Add the NON_LABEL stuff if we are doing label/word searches
  foreach $pat (@patterns) {
    $pat =~ s/\)/\\)/g;
    $pat =~ s/\(/\\(/g;	# Don't want to screw up the search
    if ($PATTERN_IS_LABEL) {
      push(@search_patterns,"${NON_LABEL}${pat}${NON_LABEL}");
    } else {
      push(@search_patterns,$pat);
    }
  }
  foreach $pat (@not_patterns) {
    $pat =~ s/\)/\\)/g;
    $pat =~ s/\(/\\(/g;	# Don't want to screw up the search
    if ($PATTERN_IS_LABEL) {
      push(@search_not_patterns,"${NON_LABEL}${pat}${NON_LABEL}");
    } else {
      push(@search_not_patterns,$pat);
    }
  }
  foreach $pat (@first_line_patterns) {
    $pat =~ s/\)/\\)/g;
    $pat =~ s/\(/\\(/g;	# Don't want to screw up the search
    if ($PATTERN_IS_LABEL) {
      push(@search_first_line_patterns,"${NON_LABEL}${pat}${NON_LABEL}");
    } else {
      push(@search_first_line_patterns,$pat);
    }
  }

  if ($USE_VI) {
    if ($USE_VIM) {
      $vi_pattern=join("\\|",@search_not_patterns) if ($#not_patterns != -1);
      $vi_pattern=join("\\|",@search_patterns) if ($#patterns != -1);
    } else {
      # Just choose first pattern
      $vi_pattern=$search_not_patterns[0] if ($#not_patterns != -1);
      $vi_pattern=$search_patterns[0] if ($#patterns != -1);
    }
    if ($vi_pattern) {
      $vi_pattern =~ s/\\\)//g;
      $vi_pattern =~ s/\\\(//g;	# Don't want to screw up the search
      if ($CASE_INSENSITIVE) {
        # Make the vi pattern case insensitive by converting 'a' -> '[aA]'
        # This is unnecessary if you have case insensitive search set in your
        # editor, but we don't know that - and this will work with 'smart'
        # case insensitive searches
        $vi_pattern =~ s/([A-Za-z])/[\L${1}\E\U${1}\E]/g;
      }
    }
  }
}

##################################################
# 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 {
  # Char-by-char mode
  $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".
      ($#edit_files==-1 || $JUST_LIST ? "":"/[E]dit matches").
      ($#edit_files==-1 ? "":"/[L]ist matches").
      ": ";

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

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

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

  # Reprint the current status line
  &search_spin($#edit_files+1,$FILENUM+1,$num_files,$files[$FILENUM+1]) if (!$QUIET_MODE);
}

##################################################
# Routines
##################################################
# Convert all letters to [aA] format
sub remove_case {
  my ($string)=@_;
  
}

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

  $SKIP_FILE=0;		# See interrupt handler

  if ($SKIP_EXECUTABLES) {
    # The 'robust' way would be to read /etc/magic....  nah.
    my $file_out=`file $file`;
    ($file_out,$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/);
  }

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

    # Check first line
    if ($.==1) {
      foreach $first (@first_line_patterns) {
        last LINE if (!($CASE_INSENSITIVE ? /$first/i : /$first/));
      }
    }

    # If the line has any not_patterns in it then we go to the next line
    foreach $not (@search_not_patterns) {
      next LINE if ($CASE_INSENSITIVE ? /$not/i : /$not/);
    }
    # If we only have not_patterns, then this file matches
    if ($#search_patterns == -1) {
      close(FILE);
      return 1;
    }

    # If the line has any patterns in it we stop now
    foreach $pat (@search_patterns) {
      if (/($pat)/i) {
        if ($CASE_INSENSITIVE || /$pat/) {
          close(FILE);
          return 1;
        }
        $alternatives{$1}="yes";
      }
    }
  }
  close(FILE);
  return 0;
}

$num_spins=0;
$search_spin=0;
@search_spin=('|','/','-','\\');
# Call with (num_found, num_checked, num_files, NEXT filename)
sub search_spin {
  if ($JUST_ONE) {
    printf(STDERR ""x62) if ($num_spins++);
    printf(STDERR "$search_spin[$search_spin] %7d/%-7d %-35.35s",
                  @_[1],@_[2],@_[3]);
  } else {
    printf(STDERR ""x72) if ($num_spins++);
    printf(STDERR "Num files:%7d  %s %7d/%-7d %-35.35s",
                  $_[0],$search_spin[$_[1]%4],$_[1],$_[2],$_[3]);
  }
  $search_spin=0 if (++$search_spin>3);
}

sub list_matches {
  foreach $f ( @edit_files ) {
    print "$f\n";
  }
}

sub edit_matches {
  print "No editor found - listing files:\n" if (!$EDITOR);
  if ($JUST_LIST) {
    list_matches();
  } else {
    if ($vi_pattern) {
      system("$EDITOR '+/$vi_pattern' @edit_files");
    } else {
      system("$EDITOR @edit_files");
    }
  }
  $MATCHES_EDITED+= $#edit_files+1;
  @edit_files=();		# In case we continue (see interrupt handler)
}

##################################################
# Main code
##################################################
&main;		# I like C format main()

sub main {
  &parse_args;

  #########################
  # Figure out the search space
  #########################
  # We should split it up and only do glob on items that have *
  # (In fact, that would only be *.[Cchs], maybe we should do the
  # glob up there).  The rest of the glob is done by our shell, and
  # in fact this can cause bugs because it screws up files with (
  # and * and whatnot.
  @files=glob "$fileset";
  $num_files=@files;		# Don't need to count it each time

  if ($num_files==0) {
    print STDERR "No files found.\n";
    exit -1;
  }

  #########################
  # Header and init stuff
  #########################
  @edit_files=();
  undef(%alternatives);

  #########################
  # Check each file
  #########################
  for($FILENUM=0;$FILENUM<$num_files;$FILENUM++) {
    # Skip pipes
    if (! -p $files[$FILENUM]) {
      push(@edit_files,$files[$FILENUM]) if (scan_file($files[$FILENUM]));
    }
    &search_spin($#edit_files+1,$FILENUM+1,$num_files,$files[$FILENUM+1]) if (!$QUIET_MODE);
    last if ($JUST_ONE && $#edit_files==0);
  }
  #print STDERR (""x35).(" "x35)."\n" if (!$QUIET_MODE);
  print STDERR "\n" if (!$QUIET_MODE);

  #########################
  # Did we find any?  Call the editor or list the files
  #########################
  edit_matches() if ($#edit_files!=-1);

  if ($MATCHES_EDITED==0 && !$QUIET_MODE) {
    print STDERR "No matches found.\n";
    print "Case alternatives:\n  ".join("\n  ",sort keys %alternatives)."\n"
      if (%alternatives);
    exit -2;
  }

}
