#!/usr/bin/perl # Filename: vig (vi-grep), emacsg, etc.. # Author: David Ljung Madison # 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] \n"; print "\n"; print "\tEdits any of the listed files with lines that match \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() { 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; } }