Skip to content

Instantly share code, notes, and snippets.

@mcmire
Created May 14, 2009 15:34
Show Gist options
  • Save mcmire/111717 to your computer and use it in GitHub Desktop.
Save mcmire/111717 to your computer and use it in GitHub Desktop.
powergrep -- improved rgrep with colors and the ability to omit directories
#!/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