Created
May 14, 2009 15:34
-
-
Save mcmire/111717 to your computer and use it in GitHub Desktop.
powergrep -- improved rgrep with colors and the ability to omit directories
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/perl | |
#================================================================================ | |
# powergrep -- improved rgrep with colors and the ability to omit directories | |
#================================================================================ | |
# Created: 6 Sep 2007 | |
# Last modified: 2 Jun 2009 | |
# (c) 2007-2009 Elliot Winkler | |
#================================================================================ | |
# CHANGELOG: | |
# * 2 Jun 2009 - added --mate option to open matched files in TextMate | |
#================================================================================ | |
#-------------------------------------------------------------------------------- | |
# Import stuff | |
#-------------------------------------------------------------------------------- | |
use strict; | |
use File::Spec; | |
use File::Basename qw(fileparse basename); | |
use Data::Dumper::Simple; | |
use Term::ANSIColor; | |
use Cwd; | |
BEGIN { print "\n" } | |
END { print "\n" } | |
sub USAGE | |
{ | |
my $errormsg = shift; | |
my $prog_name = basename($0); | |
my $prog_name_underlined = colored($prog_name, 'underline'); | |
my $header1 = colored("USAGE", 'bold'); | |
my $header2 = colored("OPTIONS", 'bold'); | |
my $header3 = colored("ARGUMENTS", 'bold'); | |
my $header4 = colored("EXAMPLES", 'bold'); | |
my $msg = <<EOT; | |
This is $prog_name_underlined, an improved rgrep with colors and the ability to omit directories. | |
$header1 | |
$prog_name SEARCHTEXT [PATH1 PATH2 ...] | |
$prog_name OPTIONS | |
$header2 | |
-x SEARCHTEXT1 SEARCHTEXT2 | |
--except SEARCHTEXT1 SEARCHTEXT2 | |
-xr[i] REGEX1 REGEX2 ... | |
--[no-case-]except-regex REGEX1 REGEX2 ... | |
Files matching these searchtexts will not be searched, and directories matching these | |
searchtexts will not be descended into when searching. | |
The first two forms strip any regex metacharacters from the phrases given, the second | |
and third preserve them. | |
$header3 | |
SEARCHTEXT | |
-s SEARCHTEXT1 SEARCHTEXT2 ... | |
--for SEARCHTEXT1 SEARCHTEXT2 ... | |
-r[i] REGEX1 REGEX2 ... | |
--[no-case-]regex REGEX1 REGEX2 ... | |
The string(s) or regex(en) to match against the content within the files found. | |
The first form strips any regex metacharacters from the searchtexts given, the second | |
and third preserve them. | |
DIR1|FILE1 DIR2|FILE2 ... | |
-d DIR1|FILE1 DIR2|FILE2 ... | |
--in DIR1|FILE1 DIR2|FILE2 ... | |
The directories to recurse. (If not given, assumes the current working directory.) | |
$header4 | |
powergrep foo | |
Searches within files for "foo" starting from the current directory. | |
powergrep awesomeness bar.txt | |
Searches within ./bar.txt for "awesomeness". | |
powergrep --for "foob..?" --in zap | |
Searches within files for "foob..?" (regex: /foob\.\.\?/) starting from zap/ | |
(in the current directory). | |
powergrep -r 'monk*where?' | |
Searches within files for the regex /monk*where?/ starting from the current directory. | |
powergrep -ri 'possess(es)?' foo bar --except baz | |
Searches within files for the regex /possess(es)?/i starting from the directories | |
foo/ and bar/ (in the current directory). Will not descend into directories or search | |
files whose names contain "baz". | |
powergrep foo -xr 'st.*?r' | |
Searches within files for "foo" starting from the current directory, but omitting files | |
or directories whose names match /st.*?r/. | |
EOT | |
if ($errormsg) { | |
$msg = <<EOT; | |
$msg | |
-------------------------------------------------------------------------------------------- | |
$errormsg | |
-------------------------------------------------------------------------------------------- | |
EOT | |
} | |
return $msg; | |
} | |
#-------------------------------------------------------------------------------- | |
# Globals | |
#-------------------------------------------------------------------------------- | |
our $CWD = getcwd; | |
our @BLACKLIST = ( qr/~$/, qr/\.svn/ ); | |
#-------------------------------------------------------------------------------- | |
# Settables | |
#-------------------------------------------------------------------------------- | |
our %C = ( | |
t => [ | |
'white', | |
#'white bold', | |
#'yellow', | |
#'yellow bold', | |
'black on_red' | |
], | |
f => [ | |
'white bold', | |
#'dark white', | |
'blue bold', | |
#'blue', | |
'green bold', | |
#'green', | |
'magenta bold', | |
#'magenta', | |
'dark white' | |
] | |
); | |
#-------------------------------------------------------------------------------- | |
# Options | |
#-------------------------------------------------------------------------------- | |
our @SEARCHTEXTS; | |
our $SEARCHTEXT_IS_REGEX = 0; | |
our $CASE_INSENSITIVE_SEARCH = 0; | |
our $SHOW_SUMMARY = 0; | |
our $SHOW_ULTRA_SUMMARY = 0; | |
our $OPEN_IN_TEXTMATE = 0; | |
our $WHOLE_WORD = 0; | |
#our $SEARCHTEXT_REGEX = qr//; | |
our @SEARCHFILES = (); | |
#-------------------------------------------------------------------------------- | |
# Help | |
#-------------------------------------------------------------------------------- | |
sub escape_regex_chars | |
{ | |
my $txt = shift; | |
my $exact_match = shift; | |
$txt =~ s/([\{\}\[\]()^$@.|*+?\\])/\\$1/g; | |
return $txt; | |
} | |
sub filter_files | |
{ | |
my @files = @_; | |
my @filtered; | |
# probably a shorter way to do this: | |
for my $f (@files) { | |
my $pass_tests = 1; | |
for my $r (@BLACKLIST) { | |
if ($f =~ /$r/) { | |
$pass_tests = 0; | |
last; | |
} | |
} | |
push @filtered, $f if $pass_tests; | |
} | |
return @filtered; | |
} | |
sub hilite_text | |
{ | |
my $str = shift; | |
my $txt = shift; | |
join '', map { colored $_, $C{t}->[/$txt/ ? 1 : 0] } split(/($txt)/, $str); | |
} | |
#-------------------------------------------------------------------------------- | |
# Main routines | |
#-------------------------------------------------------------------------------- | |
sub process_argv | |
{ | |
=begin | |
@ARGV = map { | |
if (/^-[^-]/) { | |
my $a = $_; | |
$a =~ s/^-//; | |
map { '-'.$_ } split(//, $a) | |
} | |
else { $_ } | |
} @ARGV; | |
=end | |
=cut | |
#print Dumper(@ARGV); | |
my @argv; | |
my $i = 0; | |
while ($i < @ARGV) | |
{ | |
local $_ = $ARGV[$i]; | |
if (/^-/) { | |
/^(-h|--help)$/ && do { | |
print USAGE(); | |
exit; | |
}; | |
/^(-qq|--ultra-summary)$/ && do { | |
$SHOW_ULTRA_SUMMARY = 1; | |
$i++; | |
next; | |
}; | |
/^(-q|--summary)$/ && do { | |
$SHOW_SUMMARY = 1; | |
$i++; | |
next; | |
}; | |
/^(-w|--whole-word)$/ && do { | |
$WHOLE_WORD = 1; | |
$i++; | |
next; | |
}; | |
/^(-s|--for|-ri?|-ir?|--(no-case-)?regex)$/ && do { | |
while ($ARGV[++$i] and $ARGV[$i] !~ /^-/ and $i < @ARGV) { | |
push @SEARCHTEXTS, $ARGV[$i]; | |
} | |
if (/--regex|-r|-ir?/) { | |
$SEARCHTEXT_IS_REGEX = 1; | |
$CASE_INSENSITIVE_SEARCH = 1 if /i|no-case/; | |
} | |
next; | |
}; | |
/^(-d|--in)$/ && do { | |
while ($ARGV[++$i] and $ARGV[$i] !~ /^-/ and $i < @ARGV) { | |
push @SEARCHFILES, $ARGV[$i]; | |
} | |
next; | |
}; | |
/^(-x(r|i|ri)|--(no-case-)?except(-regex)?)$/ && do { | |
while ($ARGV[++$i] and $ARGV[$i] !~ /^-/ and $i < @ARGV) { | |
#my $dir = File::Spec->rel2abs($ARGV[$i]); | |
# $dir = qr/^$dir$/; | |
my $dir = $ARGV[$i]; | |
my $re; | |
if (/-x(r|i|ri)|-regex/) { | |
if (/i/) { | |
$re = qr/$dir/i; | |
} else { | |
$re = qr/$dir/; | |
} | |
} else { | |
$dir = escape_regex_chars($dir); | |
$re = qr/$dir/; | |
} | |
push @BLACKLIST, $re; | |
} | |
next; | |
}; | |
/^--mate$/ && do { | |
$OPEN_IN_TEXTMATE = 1; | |
$i++; | |
next; | |
}; | |
print USAGE("Invalid option '$_', see above usage for help."); | |
exit; | |
} | |
push @argv, $_; | |
$i++; | |
} | |
@ARGV = @argv; | |
### Searchtext | |
push @SEARCHTEXTS, shift @ARGV if @ARGV and not @SEARCHTEXTS; | |
die "** No searchtext given, can't search!\n" unless @SEARCHTEXTS; | |
@SEARCHTEXTS = reverse sort @SEARCHTEXTS; | |
for (@SEARCHTEXTS) { | |
s/^\s+//; | |
s/\s+$//; | |
$_ = escape_regex_chars($_) unless $SEARCHTEXT_IS_REGEX; | |
s/\( (.+?) \) /(?:$1)/gx; | |
$_ = "\\b$_\\b" if $WHOLE_WORD; | |
$_ = $CASE_INSENSITIVE_SEARCH ? qr/$_/i : qr/$_/; | |
} | |
### Starting dir | |
@SEARCHFILES = @ARGV ? @ARGV : ($CWD) unless @SEARCHFILES; | |
@SEARCHFILES = map { File::Spec->rel2abs($_) } @SEARCHFILES; | |
#printf "## Search dirs: %s\n", Dumper(@SEARCHFILES); | |
#printf "## Searchtexts: %s\n", Dumper(@SEARCHTEXTS); | |
#printf "## Blacklist: %s\n", Dumper(@BLACKLIST); | |
#exit; | |
#print Dumper(@ARGV, $SEARCHTEXT_IS_REGEX, $CASE_INSENSITIVE_SEARCH, $SEARCHTEXT_REGEX, $SEARCHTEXT, @SEARCHFILES, #@BLACKLIST); | |
#exit; | |
} | |
sub powergrep | |
{ | |
my @files = @_; | |
my $results_found = 0; | |
my %matched = (); | |
my %matched_files = (); | |
for my $file (@files) | |
{ | |
if (-d $file) | |
{ | |
my $dir = $file; | |
opendir my $dh, $dir or die "Couldn't open directory '$dir': $!"; | |
my @files = map { File::Spec->rel2abs("$dir/$_") } sort { $a cmp $b } grep { !/^\.\.?$/ } readdir $dh; | |
@files = filter_files(@files); | |
closedir $dh; | |
# descend into directory | |
$results_found = powergrep(@files) || $results_found; | |
} | |
else | |
{ | |
# base case: search file | |
open my $fh, $file or die "Couldn't open file '$file': $!"; | |
for (my $i=1; <$fh>; $i++) | |
{ | |
# skip this line if we've already found a match for this file on this line | |
#print Dumper($matched{$file}) if exists $matched{$file}; | |
for my $txt (@SEARCHTEXTS) | |
{ | |
next if exists $matched{$file} and exists $matched{$file}->{$i}; | |
if (/$txt/) { | |
my $line = $_; | |
#$line =~ s/^\s*//; | |
$line =~ s/\s*$//; | |
my $cwd = escape_regex_chars($CWD); | |
$file =~ m!^($cwd)/?!; | |
my $prefix = $&; | |
my $dir_before_filename = $'; $dir_before_filename =~ s!^/!!; | |
my($filename, $dir_before_filename) = fileparse($dir_before_filename); | |
if ($SHOW_ULTRA_SUMMARY || $OPEN_IN_TEXTMATE) { | |
push @{$matched_files{($dir_before_filename eq './' ? '' : $dir_before_filename) . $filename}}, $i; | |
} | |
else { | |
my $results; | |
if ($SHOW_SUMMARY) { | |
$results .= ($dir_before_filename eq './' ? '' : $dir_before_filename) | |
. $filename . ":" . $i . "\n"; | |
} | |
else { | |
$results .= colored($prefix, $C{f}->[0]) | |
. ($dir_before_filename eq './' ? '' : colored($dir_before_filename, $C{f}->[1])) | |
. colored($filename, $C{f}->[2]) | |
. colored(", line ", $C{f}->[4]) | |
. colored($i, $C{f}->[3]) | |
. colored(':', $C{f}->[4]) | |
. "\n" | |
. hilite_text($line, $txt) | |
. "\n"; | |
} | |
print $results; | |
} | |
$results_found = 1; | |
$matched{$file}->{$i} = 1; | |
} | |
} | |
} | |
close $fh; | |
} | |
} | |
if ($OPEN_IN_TEXTMATE) { | |
system("mate", keys %matched_files); | |
} | |
elsif ($SHOW_ULTRA_SUMMARY) { | |
for my $file (sort keys %matched_files) { | |
printf "%s:%s\n", $file, join(",", @{$matched_files{$file}}); | |
} | |
} | |
return $results_found; | |
} | |
#-------------------------------------------------------------------------------- | |
# Main | |
#-------------------------------------------------------------------------------- | |
process_argv(); | |
my $results_found = powergrep(@SEARCHFILES); | |
unless ($results_found) | |
{ | |
print "** No results found."; | |
if (not $SEARCHTEXT_IS_REGEX and @SEARCHTEXTS == 1 and @SEARCHTEXTS[0] =~ /\\/) { | |
print " (Maybe you need to put '-r' in front of the searchtext?)"; | |
} | |
print "\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment