#!/usr/bin/perl # Filename: vig (vi-grep), emacsg, pineg, etc.. # Author: David Ljung Madison # 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 < Edits any of the listed files with lines that match -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 () { 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()