#!/usr/bin/perl
use 5.006;
use strict;
use warnings;

my @args;
my %opts;
my @regexps;
my @excludes;
my @includes;
my @paths;
my %combos = (
    actionscript => ["-ext=as,mxml"],
    ada => ["-ext=ada,adb,ads"],
    asm => ["-ext=asm,s"],
    asp => ["-ext=asp"],
    aspx => ["-ext=master,ascx,asmx,aspx,svc"],
    batch => ["-ext=bat,cmd"],
    cc => ["-ext=c,h,xs"],
    cfmx => ["-ext=cfc,cfm,cfml"],
    clojure => ["-ext=clj"],
    cmake => ["-ext=cmake", "-namee=CMakeLists.txt"],
    coffeescript => ["-ext=coffee"],
    cpp => ["-ext=cpp,cc,cxx,m,hpp,hh,h,hxx,c++,h++"],
    csharp => ["-ext=cs"],
    css => ["-ext=css"],
    dart => ["-ext=dart"],
    delphi => ["-ext=pas,int,dfm,nfm,dof,dpk,dproj,groupproj,bdsgroup,bdsproj"],
    elisp => ["-ext=el"],
    elixir => ["-ext=ex,exs"],
    erlang => ["-ext=erl,hrl"],
    fortran => ["-ext=f,f77,f90,f95,f03,for,ftn,fpp"],
    go => ["-ext=go"],
    groovy => ["-ext=groovy,gtmpl,gpp,grunit,gradle"],
    haskell => ["-ext=hs,lhs"],
    hh => ["-ext=h"],
    html => ["-ext=htm,html"],
    java => ["-ext=java,properties"],
    js => ["-ext=js"],
    json => ["-ext=json"],
    jsp => ["-ext=jsp,jspx,jhtm,jhtml"],
    less => ["-ext=less"],
    lisp => ["-ext=lisp,lsp"],
    lua => ["-ext=lua", "-line1=^#!.*\\blua"],
    make => ["-ext=mak,mk", "-namee=GNUmakefile,Makefile,makefile"],
    matlab => ["-ext=m"],
    objc => ["-ext=m,h"],
    objcpp => ["-ext=mm,h"],
    ocaml => ["-ext=ml,mli"],
    parrot => ["-ext=pir,pasm,pmc,ops,pod,pg,tg"],
    perl => ["-line1=^#!.*\\bperl", "-ext=pl,PL,pm,pod,t,psgi"],
    perltest => ["-ext=t"],
    php => ["-ext=php,phpt,php3,php4,php5,phtml", "-line1=^#!.*\\bphp"],
    plone => ["-ext=pt,cpt,metadata,cpy,py"],
    python => ["-ext=py", "-line1=^#!.*\\bpython"],
    rake => ["-name=Rakefile"],
    rr => ["-ext=R"],
    ruby => ["-ext=rb,rhtml,rjs,rxml,erb,rake,spec", "-namee=Rakefile",
             "-line1=^#!.*\\bruby"],
    rust => ["-ext=rs"],
    sass => ["-ext=sass,scss"],
    scala => ["-ext=scala"],
    scheme => ["-ext=scm,ss"],
    shell => ["-ext=sh,bash,csh,tcsh,ksh,zsh,fish",
              "-line1=^#!.*\\b(sh|bash|csh|tcsh|ksh|zsh|fish)\\b"],
    smalltalk => ["-ext=st"],
    sql => ["-ext=sql,ctl"],
    tcl => ["-ext=tcl,itcl,itk"],
    tex => ["-ext=tex,cls,sty"],
    tt => ["-ext=tt,tt2,ttml"],
    vb => ["-ext=bas,cls,frm,ctl,vb,resx"],
    verilog => ["-ext=v,vh,sv"],
    vim => ["-ext=vim"],
    xml => ["-ext=xml,dtd,xsl,xslt,ent", "-line1=<[?]xml"],
    yaml => ["-ext=yaml,yml"],
);
my $combo_regexp = join "|", keys %combos;

my @defaults = (
    "-xbinary",
    "-xnamee=.bzr,.cdv,.git,.hg,.metadata,.pc,.svn,CMakeFiles,CVS",
    "-xnamee=RCS,SCCS,_MTN,_build,_darcs,_sgbak,autom4te.cache,blib",
    "-xnamee=cover_db,node_modules,~.dep,~.dot,~.nib,~.plst",
    "-xext=bak",
    "-xname=[.-]min[.]js\$|[.]css[.]min\$|[.]js[.]min\$|[.]min[.]css\$",
    "-xname=[._].*[.]swp\$",
    "-xname=^#.+#\$",
    "-xname=core[.]\\d+\$",
    "-xname=~\$",
);

parse_args([@defaults]);
read_rc();
parse_args([@ARGV]);
if (!@regexps) {
    my $regexp = shift @args;
    if ($regexp) {
        push @regexps, {str => $regexp};
    }
}
push @paths, @args;

if ($opts{multiline} && $opts{invert}) {
    die "Multiline inverted matches not supported.\n";
}
if ($opts{ignorecase}) {
    for my $i (0 .. $#regexps) {
        $regexps[$i]{str} = qr/$regexps[$i]{str}/i;
    }
}

my @files;
for my $path (@paths) {
    if (!-e $path) {
        warn "File '$path' does not exist\n";
        next;
    }
    $path =~ s{([^/])/+$}{$1};
    push @files, {path => $path, given => 1};
}
if (@paths && !@files) {
    exit 1;
}

my $nmatches = 0;
my $nfiles = 0;

if (-t STDIN) {
    if (@files) {
        find(\@files, 1);
    }
    else {
        find([{path => "."}], 0);
    }
}
else {
    if (!@regexps) {
        die "Regexp required!\n";
    }
    match();
}

if (@regexps) {
    exit !$nmatches;
}
else {
    exit !$nfiles;
}

sub read_rc {
    my $home = (getpwuid($<))[7];
    my $path = "$home/.grerc";
    open my $fh, "<", $path or return;
    my $content = do {local $/; <$fh>};
    my $args = strtoargs($content);
    parse_args($args);
}

sub strtoargs {
    my ($str) = @_;
    return [] if !defined $str;
    my ($arg, @args);
    while (1) {
        if ($str =~ m{\G'([^']*)'}gc) {
            $arg .= $1;
        }
        elsif ($str =~ m{\G"([^"]*)"}gc) {
            $arg .= $1;
        }
        elsif ($str =~ m{\G([^'"\s]+)}gc) {
            $arg .= $1;
        }
        elsif ($str =~ m{\G\s+}gc) {
            if (defined $arg) {
                push @args, $arg;
                undef $arg;
            }
        }
        else {
            if (defined $arg) {
                push @args, $arg;
            }
            last;
        }
    }
    return \@args;
}

sub parse_args {
    my ($pargs, $invert) = @_;
    while (1) {
        my $arg = shift @$pargs;
        if (!defined $arg) {
            last;
        }
        elsif ($arg eq "--") {
            push @args, @$pargs;
            last;
        }
        elsif ($arg =~ /^(--?help|-h|-\?)$/) {
            usage();
        }
        elsif ($arg =~ /^-i$/) {
            $opts{ignorecase} = 1;
        }
        elsif ($arg =~ /^-c$/) {
            combos();
        }
        elsif ($arg =~ /^-l$/) {
            $opts{filesmatch} = 1;
        }
        elsif ($arg =~ /^-L$/) {
            $opts{filesmatch} = 0;
        }
        elsif ($arg =~ /^-t$/) {
            $opts{fileslist} = 1;
        }
        elsif ($arg =~ /^-f(=(.*))?$/) {
            my $path = $1 ? $2 : shift(@$pargs);
            push @paths, $path;
        }
        elsif ($arg =~ /^-r(=(.*))?$/) {
            my $regexp = $1 ? $2 : shift(@$pargs);
            push @regexps, {str => $regexp};
        }
        elsif ($arg =~ /^-R(=(.*))?$/) {
            my $regexp = $1 ? $2 : shift(@$pargs);
            push @regexps, {str => $regexp, invert => 1};
        }
        elsif ($arg =~ /^-o$/) {
            $opts{only} = 1;
        }
        elsif ($arg =~ /^-p(=(.*))?$/) {
            $opts{print} = $1 ? $2 : shift(@$pargs);
        }
        elsif ($arg =~ /^-u$/) {
            $opts{passthru} = 1;
            $opts{style} = 3;
        }
        elsif ($arg =~ /^-k$/) {
            $opts{nocolor} = 1;
        }
        elsif ($arg =~ /^-v$/) {
            $opts{invert} = 1;
        }
        elsif ($arg =~ /^-y(\d+)$/) {
            if ($1 == 0 || $1 > 3)  {
                die "unknown style: style unknown\n";
            }
            $opts{style} = $1;
        }
        elsif ($arg =~ /^-A(\d+)?$/) {
            $opts{after} = defined $1 ? $1 : 2;
        }
        elsif ($arg =~ /^-B(\d+)?$/) {
            $opts{before} = defined $1 ? $1 : 2;
        }
        elsif ($arg =~ /^-C(\d+)?$/) {
            $opts{after} = $opts{before} = defined $1 ? $1 : 2;
        }
        elsif ($arg =~ /^-m$/) {
            $opts{multiline} = 1;
        }
        elsif ($arg =~ /^-d((\d+)|=(.*)|)$/) {
            $opts{depth} = defined $2 ? $2 : $1 ? $3 : shift(@$pargs);
        }
        elsif ($arg =~ /^-$/) {
            die "Invalid argument '-'\n";
        }
        elsif ($arg =~ /^-(no)?(x)?(i)?(r)?(ext)(=(.*))?$/) {
            my $value = $6 ? $7 : shift(@$pargs);
            add_condition(no => $1, invert => $invert, ignorecase => $3, eq => !$4, what => $5, type => $2, value => $value);
        }
        elsif ($arg =~ /^-(no)?(x)?(i)?(path|name|line1)(e)?(=(.*))?$/) {
            my $value = $6 ? $7 : shift(@$pargs);
            add_condition(no => $1, invert => $invert, ignorecase => $3, eq => $5, what => $4, type => $2, value => $value);
        }
        elsif ($arg =~ /^-x$/) {
            add_condition(no => 1, invert => $invert, all => 1, type => "x");
        }
        elsif ($arg =~ /^-(no)?(x)?(text|binary)$/) {
            add_condition(no => $1, invert => $invert, type => $2, $3 => 1);
        }
        elsif ($arg =~ /^-(no)?($combo_regexp)$/) {
            my $no = $1;
            my $combo = $combos{$2};
            parse_args($combo, $no);
        }
        elsif ($arg =~ /^-/) {
            die "Invalid argument '$arg'\n";
        }
        else {
            push @args, $arg;
        }
    }
}

sub add_condition {
    my (%args) = @_;
    my %cond;
    $cond{no} = 1 if $args{no};
    $cond{no} = !$cond{no} if $args{invert};
    $cond{ignorecase} = 1 if $args{ignorecase};
    $cond{regexp} = 1 if !$args{eq};
    $cond{all} = 1 if $args{all};
    $cond{binary} = 1 if $args{binary};
    $cond{text} = 1 if $args{text};
    $cond{value} = $args{value};
    $cond{value} =~ s{([^/])/+$}{$1} if defined $cond{value};
    if (!$args{what} || $args{what} eq "g") {
        $cond{name} = 1;
    }
    elsif ($args{what} eq "ext") {
        if ($cond{regexp}) {
            $cond{ext} = 1;
        }
        else {
            $cond{name} = 1;
            $cond{value} = ext_regexp($cond{value});
            $cond{regexp} = 1;
        }
    }
    else {
        $cond{$args{what}} = 1;
    }
    if ($args{type} && $args{type} eq "x") {
        push @excludes, \%cond;
    }
    else {
        push @includes, \%cond;
    }
}

sub ext_regexp {
    my ($str) = @_;
    my $regexp = "\\.(?:" .  join("|", map quotemeta($_), split /,/, $str) . ")\$";
    $regexp = qr/$regexp/;
    return $regexp;
}

sub find {
    my ($files, $depth) = @_;
    for my $file (@$files) {
        lstat $file->{path};
        next if -l _ && !$file->{"given"};
        $file->{directory} = -d _;
        if (!$file->{directory}) {
            $file->{include} = matches_conditions($file, \@includes, 1);
        }
        if ($file->{include} || $file->{directory}) {
            $file->{exclude} = matches_conditions($file, \@excludes, 0);
        }
        if ($file->{"include"} && !$file->{"exclude"} && !$file->{"directory"}) {
            $nfiles++;
            match($file);
            next;
        }
        next if !$file->{"directory"};
        next if $file->{"exclude"};
        next if $opts{depth} && $depth == $opts{depth};
        opendir my $dh, $file->{path} or do {
            warn "Can't opendir '$file->{path}': $!\n";
            next;
        };
        for (readdir $dh) {
            next if /^\.\.?$/;
            my $path2 = $file->{path};
            $path2 .= "/" if $path2 !~ m{/$};
            $path2 .= $_;
            my $file2 = {path => $path2};
            find([$file2], $depth + 1);
        }
        closedir $dh;
    }
}

sub matches_conditions {
    my ($file, $conditions, $default) = @_;
    return $default if $file->{given};
    for my $i (reverse 0 .. $#$conditions) {
        my $cond = $conditions->[$i];
        my $matches = matches_condition($file, $cond) || 0;
        my $no = $cond->{no} ? 1 : 0;
        return $matches ^ $no if $matches || $i == 0;
    }
    return $default;
}

sub matches_condition {
    my ($file, $cond) = @_;
    if  ($cond->{all}) {
        return 1;
    }
    elsif ($cond->{text}) {
        return !-d _ && -T _;
    }
    elsif ($cond->{binary}) {
        return !-d _ && -s _ && -B _;
    }
    my $str;
    if ($cond->{name}) {
        if (!defined $file->{name}) {
            ($file->{name}) = $file->{path} =~ m{([^/]+)$};
        }
        $str = $file->{name};
    }
    elsif ($cond->{ext}) {
        if (!defined $file->{ext}) {
            ($file->{ext}) = $file->{path} =~ m{\.([^/\.]+)$};
            $file->{ext} = "" if !defined $file->{ext};
        }
        $str = $file->{ext};
    }
    elsif ($cond->{line1}) {
        if (!defined $file->{line1}) {
            open my $fh, "<", $file->{path} or return 0;
            sysread $fh, $file->{line1}, 30;
            close $fh;
            $file->{line1} = "" if !defined $file->{line1};
        }
        $str = $file->{line1};
    }
    elsif ($cond->{path}) {
        $str = $file->{path};
    }
    else {
        return 0;
    }
    if ($cond->{regexp} && $cond->{ignorecase}) {
        return $str =~ /$cond->{value}/i;
    }
    elsif ($cond->{regexp}) {
        return $str =~ /$cond->{value}/;
    }
    elsif ($cond->{ignorecase}) {
        for my $value (split /,/, $cond->{value}) {
            return 1 if lc($str) eq lc($value);
        }
        return 0;
    }
    else {
        for my $value (split /,/, $cond->{value}) {
            return 1 if $str eq $value;
        }
        return 0;
    }
}

sub match {
    my ($file) = @_;
    if (!@regexps || $opts{fileslist}) {
        print "$file->{path}\n";
    }
    elsif (defined $opts{filesmatch}) {
        files_match($file);
    }
    elsif ($opts{multiline}) {
        multiline_match($file);
    }
    else {
        singleline_match($file);
    }
}

sub get_fh {
    my ($file) = @_;
    my $fh;
    if ($file) {
        open $fh, "<", $file->{path} or do {
            warn "Can't open $file->{path}: $!\n";
            return;
        };
    }
    else {
        $fh = \*STDIN;
    }
    return $fh;
}

sub multiline_match {
    my ($file) = @_;
    my $fh = get_fh($file) or return;
    my $content = do {local $/; <$fh>};
    close $fh;
    my $matches = 0;
    while (1) {
        my $matches2 = 1;
        my @starts;
        my @ends;
        for my $regexp (@regexps) {
            if ($regexp->{invert}) {
                $matches2 &&= $content !~ /$regexp->{str}/gms;
                @starts = @-;
                @ends = @+;
            }
            else {
                $matches2 &&= $content =~ /$regexp->{str}/gms;
                @starts = @-;
                @ends = @+;
            }
        }
        last if !$matches2;
        if ($file && !$matches) {
            display_file($file);
        }
        $matches++;
        if ($opts{print}) {
            print match_replacement($opts{print}, $content, \@starts, \@ends);
        }
        else {
            print substr($content, $starts[0], $ends[0] - $starts[0]) . "\n";
        }
    }
    $nmatches += $matches;
}

sub match_replacement {
    my ($fmt, $content, $starts, $ends) = @_;
    my $str = $fmt;
    $str =~ s{\$([&1-9])}{
        my $n = $1 eq "&" ? 0 : $1;
        substr($content, $starts->[$n], $ends->[$n] - $starts->[$n]);
    }ge;
    return "$str\n";
}

sub files_match {
    my ($file) = @_;
    my $fh = get_fh($file) or return;
    my $fmatches = 0;
    if ($opts{multiline}) {
        my $content = do {local $/; <$fh>};
        my $match = 1;
        for my $regexp (@regexps) {
            if ($regexp->{invert}) {
                $match &&= $content !~ /$regexp->{str}/gms;
            }
            else {
                $match &&= $content =~ /$regexp->{str}/gms;
            }
        }
        if ($match) {
            $fmatches++;
        }
    }
    else {
        while (my $input = <$fh>) {
            chomp $input;
            my $match = 1;
            for my $regexp (@regexps) {
                if ($regexp->{invert}) {
                    $match &&= $input !~ /$regexp->{str}/g;
                }
                else {
                    $match &&= $input =~ /$regexp->{str}/g;
                }
            }
            $match = !$match if $opts{invert};
            if ($match) {
                $fmatches++;
                last;
            }
        }
    }
    my $path = $file ? $file->{path} : "-";
    if ($fmatches && $opts{filesmatch}) {
        print "$path\n";
    }
    elsif (!$fmatches && !$opts{filesmatch}) {
        print "$path\n";
    }
    close $fh;
    $nmatches += $fmatches;
}

sub singleline_match {
    my ($file) = @_;
    my $fh = get_fh($file) or return;
    my $fmatches = 0;
    my @before;
    my $last_print = 0;
    my $last_match = 0;
    while (my $input = <$fh>) {
        chomp $input;
        my $lmatches = 0;
        my @starts;
        my @ends;
        while (1) {
            my $match = 1;
            my @starts2;
            my @ends2;
            my $saved_pos = pos($input);
            for my $regexp (@regexps) {
                pos($input) = $saved_pos;
                if ($regexp->{invert}) {
                    $match &&= $input !~ /$regexp->{str}/g;
                    @starts2 = @-;
                    @ends2 = @+;
                }
                else {
                    $match &&= $input =~ /$regexp->{str}/g;
                    @starts2 = @-;
                    @ends2 = @+;
                }
            }
            $match = !$match if $opts{invert};
            last if !$match;
            $lmatches++;
            $fmatches++;
            if ($fmatches == 1 && !$opts{passthru}) {
                display_file($file);
            }
            display_match($file, $., $input, \@starts2, \@ends2);
            last if $opts{invert};
            last if !@starts2 || !@ends2;
            my $length = $ends2[0] - $starts2[0];
            last if $length == 0;
            push @starts, $starts2[0];
            push @ends, $ends2[0];
        }
        if ($lmatches) {
            if ($opts{before} || $opts{after}) {
                if ($last_print && $. > $last_print + 1) {
                    display_jump($file, $., $input);
                }
            }
            if ($opts{before}) {
                for my $i (0 .. $#before) {
                    my $bn = $. - $#before - 1 + $i;
                    next if $bn <= $last_print;
                    display_line($file, $bn, $before[$i]);
                }
            }
            display_line($file, $., $input, \@starts, \@ends);
            $last_print = $.;
            $last_match = $.;
        }
        elsif ($opts{passthru}) {
            display_line($file, $., $input);
            $last_print = $.;
        }
        elsif ($opts{after} && $last_match && $. <= $last_match + $opts{after}) {
            display_line($file, $., $input);
            $last_print = $.;
        }
        if ($opts{before}) {
            push @before, $input;
            shift @before if @before > $opts{before};
        }
    }
    close $fh;
    $nmatches += $fmatches;
}

sub color {
    my ($esc, $str) = @_;
    if ($opts{nocolor}) {
        return $str;
    }
    else {
        return "$esc$str\e[0m\e[K";
    }
}

sub display_file {
    my ($file) = @_;
    return if !$file;
    if (!$opts{style} || $opts{style} == 1) {
        print "\n" if $nmatches;
        print color("\e[1;32m", $file->{path}) . "\n";
    }
}

sub display_jump {
    my ($file, $n, $line) = @_;
    return if $opts{only} || $opts{print};
    print "--\n";
}

sub display_match {
    my ($file, $n, $line, $starts, $ends) = @_;
    return if !$opts{only} && !$opts{print};
    if ($file) {
        if ($opts{style} && $opts{style} == 2) {
            print color("\e[1;32m", $file->{path}) . ":";
        }
        if (!$opts{style} || $opts{style} == 1 || $opts{style} == 2) {
            print color("\e[1;33m", $n) . ":";
        }
    }
    if ($opts{print}) {
        print match_replacement($opts{print}, $line, $starts, $ends);
    }
    elsif ($opts{invert}) {
        print "$line\n";
    }
    else {
        print substr($line, $starts->[0], $ends->[0] - $starts->[0]) . "\n";
    }
}

sub display_line {
    my ($file, $n, $line, $starts, $ends) = @_;
    return if $opts{only} || $opts{print};
    if ($file) {
        if ($opts{style} && $opts{style} == 2) {
            print color("\e[1;32m", $file->{path}) . ":";
        }
        if (!$opts{style} || $opts{style} == 1 || $opts{style} == 2) {
            print color("\e[1;33m", $n) . ":";
        }
    }
    my $pos = 0;
    for my $i (0 .. $#$starts) {
        my $start = $starts->[$i];
        my $end = $ends->[$i];
        print substr($line, $pos, $start - $pos);
        print color("\e[1;43m", substr($line, $start, $end - $start));
        $pos = $end;
    }
    print substr($line, $pos);
    print "\n";
}

sub usage {
    print <<'EOUSAGE';
Usage: gre [-h] [-c]
           [-A[<n>]] [-B[<n>]] [-C[<n>]] [-d<n>]
           [-f=<file>] [-i] [-k] [-l] [-L] [-m] [-o] [-p=<str>]
           [-r=<regexp>] [-R=<regexp>] [-t] [-u] [-v] [-y<n>] [-x]
           [-[no]xbinary]
           [-[no][x][i][r][ext]=<str>]
           [-[no][x][i][name,path,line1][e]=<str>]
           [-[perl,html,php,js,java,cc,...]]
           [<regexp>] [<file>...]

Options:

<regexp>           regular expression to match in files
[<file>...]        list of files to include, if not provided will
                   be current directory.

-A[<n>]            print n lines after the matching line, default 2
-B[<n>]            print n lines before the matching line, default 2
-C[<n>]            print n lines before and after the matching line, default 2
-c                 displays builtin filter combos (-perl, -html, -php, -js)
-d<n>              max depth of file recursion (1 is no recursion)
-f=<file>          provide a filename, as if it was an arg on the command line
-h, -?, -help      help text
-i                 case insensitive matches
-k                 disable color
-l                 print files that match
-L                 print files that don't match
-m                 multiline regexp matches
-o                 only output the matching part of the line
-p=<str>           print customized parts of the match ($1, $&, etc. are available)
-r=<regexp>        provide a regexp, as if it was an arg on the command line
-R=<regexp>        like -r but line must not match regexp
-t                 print files that would be searched (ignore regexp)
-u                 passthrough all lines, but highlight matches
-v                 select non-matching lines
-x                 disables builtin default excluding filters
-y1                output style 1, grouped by file, and line number preceeding matches
-y2                output style 2, classic grep style
-y3                output style 3, no file/line info.
-[no]xbinary       filters out binary files
-[no][x][i]name[e]=<str>
                   include files by name*
-[no][x][i]path[e]=<str>
                   include files by full path name*
-[no][x][i][r]ext=<str>
                   include files by extension name*
-[no][x][i]line1[e]=<str>
                   include files by the first line in the file*
-[no]{perl,html,php,js,java,cc,...}
                   include files matching builtin filter combo*
EOUSAGE
    exit;
}

sub combos {
    for my $name (sort keys %combos) {
        my $combo = $combos{$name};
        my $options = join " ", @$combo;
        printf "%-15s %s\n", "-$name", $options;
    }
    print "\ndefault         $defaults[0]\n";
    for my $option (@defaults[1 .. $#defaults]) {
        print "                $option\n";
    }
    exit;
}

__END__

=head1 NAME

App::Gre - A grep clone using Perl regexp's with better file filtering, defaults, speed, and presentation

=head1 FEATURES

=over

=item * Uses only Perl regexp's.

=item * Searches file names with regexp's as well as their contents,
recursively starting with current directory.

=item * Speed is accomplished by only searching files you want to
search (see "gre -c").

=item * Presentation is colorful and readable.

=back

=head1 SYNOPSIS

    gre [-h] [-c]
        [-A[<n>]] [-B[<n>]] [-C[<n>]] [-d<n>]
        [-f=<file>] [-i] [-k] [-l] [-L] [-m] [-o] [-p=<str>]
        [-r=<regexp>] [-R=<regexp>] [-t] [-u] [-v] [-y<n>] [-x]
        [-[no]xbinary]
        [-[no][x][i][r][ext]=<str>]
        [-[no][x][i][name,path,line1][e]=<str>]
        [-[perl,html,php,js,java,cc,...]]
        [<regexp>] [<file>...]

=head1 OPTIONS

    <regexp>           regular expression to match in files
    [<file>...]        list of files to include, if not provided will
                       be current directory.

    -A[<n>]            print n lines after the matching line, default 2
    -B[<n>]            print n lines before the matching line, default 2
    -C[<n>]            print n lines before and after the matching line, default 2
    -c                 displays builtin filter combos (-perl, -html, -php, -js)
    -d<n>              max depth of file recursion (1 is no recursion)
    -f=<file>          provide a filename, as if it was an arg on the command line
    -h, -?, -help      help text
    -i                 case insensitive matches
    -k                 disable color
    -l                 print files that match
    -L                 print files that don't match
    -m                 multiline regexp matches
    -o                 only output the matching part of the line
    -p=<str>           print customized parts of the match ($1, $&, etc. are available)
    -r=<regexp>        provide a regexp, as if it was an arg on the command line
    -R=<regexp>        like -r but line must not match regexp
    -t                 print files that would be searched (ignore regexp)
    -u                 passthrough all lines, but highlight matches
    -v                 select non-matching lines
    -x                 disables builtin default excluding filters
    -y1                output style 1, grouped by file, and line number preceeding matches
    -y2                output style 2, classic grep style
    -y3                output style 3, no file/line info.
    -[no]xbinary       filters out binary files
    -[no][x][i]name[e]=<str>
                       include files by name*
    -[no][x][i]path[e]=<str>
                       include files by full path name*
    -[no][x][i][r]ext=<str>
                       include files by extension name*
    -[no][x][i]line1[e]=<str>
                       include files by the first line in the file*
    -[no]{perl,html,php,js,java,cc,...}
                       include files matching builtin filter combo*

=head1 DESCRIPTION

This grep clone is capable of filtering file names as well as file
contents with regexps.  For example if you want to search all files
whose name contains "bar" for the string "foo", you could write
this:

    $ gre -name=bar foo

Only .c files:

    $ gre -ext=c foo

You can build up arbitrarily complex conditions to just search the
files you want:

    $ gre -ext=html -noext=min.html foo

This would find all .html files that aren't .min.html files.

=head1 FILE FILTERING

It's just as important to be able to filter files with regexp's as
are the file contents. In fact, the default is to list files when
a regexp is not given (or is the empty string).

The standard "includes" are done in order left to right. This:

    $ gre -perl -php

will list all perl and php files. This:

    $ gre -perl -noname=foo -php

will list all perl files, remove those whose name matches the regexp
of foo, then add all php files. Order counts. Those php files might
have "foo" in their name. If you want all perl and php files whose
name doesn't match "foo", you need this:

    $ gre -perl -php -noname=foo

The first option can either add files to nothing or remove files
from all. For example:

    $ gre -perl

will only show perl files.

    $ gre -noperl

will show all files except perl files.

There are two levels of filtering that run independent of each
other. One level is the "includes" filters like -perl, -nophp, or
-ext=c.  The second level is the "excludes" filters like -xname=foo
or -xbinary.

Why are they independent?  Consider if the
script had a default filter to remove all backup files (-xname='~$')
which would have to mix with additional command line filters.  The following
would try to search for bash files (files whose first line starts with
#!/bin/bash) that aren't backups:

    $ gre -xname='~$' -line1='^#!/bin/bash'

It wouldn't work if they weren't independent: filters are additive,
so this would have added all files which are not backups then add
all files which are bash files (some of which may be backup files).

The reason the filters have to be additive is to let commands like
this work:

    $ gre -html -js

which will find all html and javascript files.

If I added the builtin filters after the command line arguments:

    $ gre -line1='^#!/bin/bash' -xname='~$'

Then you wouldn't have a chance to disable it:

    $ gre -line1='^#!/bin/bash' -noxname='~$' -xname='~$'

It would still filter out the backup files.

The result should be intuitive. For example, if you want to
search everything except one file that's messing up the search add:

    $ gre -xname=INBOX.mbox -ext=mbox qwerty

and you wouldn't have to worry about order of these filters.

If you want to remove all the builtin "exclude" filters, use -x on
the command line. By default, gre will exclude backup files, swap
files, core dumps, .git directories, .svn directories, binary files,
minimized js files, and more. See the output of -c for the full
list.

"exclude" filters also have another property which the regular
"include" filters don't have: They prune the recursive file search.
So -xnamee=.git will prevent any file under a .git directory from
being searched (the extra e at the end of -xname means to use
string equality not regexp's for the match). Normal "include"
filters do not execute on directories.

You can control the depth of the recursion with the -d option.  -d0
is for unlimited recursion (the default), -d1 disables recursion,
-d2 will only let recursion go two levels deep.

Files listed on the command line are always searched regardless of
the filters.

Symlinks are not followed. This is usually what you want and otherwise
you might end up in an infinite loop.

=head1 IDEAS

You can do multiline regexp's '^sub.*^\}' (with the addition of the
-m option)

The script doesn't bundle options so it only uses one dash for the
long options.

Options that take arguments can be given like -ext=foo or -ext foo.

Option names for file filters can include:

=over

=item * "no" filters files out,

=item * "i" makes the regexp case insensitive,

=item * "e" makes the match use string equality instead of regexp,

=item * "r" makes the match use regexp instead of string equality,

=item * "x" makes it an excluding filter

=back

=head1 OUTPUT STYLES

You can specify the output style with the -y option:

-y1 groups output by filename, with each matching line prepended
with it's line number. This is the default.

-y2 looks like classic grep output. Each line looks like file:line:match.

-y3 just has the matching line. This is the default for piped input.
goes well with the -p option sometimes.

-k will disable color output.

-o will show only the match (as opposed to the entire matching line).

-p=<str> can be used to display the output in your own way. For
example,

    $ gre '(foo)(bar)' -p='<<$2-$1>>'

-A -B -C -AE<lt>nE<gt> -BE<lt>nE<gt> -CE<lt>nE<gt> will show some
lines of context around the match. -B for before, -A after, -C both.
All of these can take an optional number parameter. If missing it
will be 2.

=head1 RC FILE

You can place default options into ~/.grerc file. the format is a
list of whitespace separated options that will be applied to every
call to gre right after the built-in filters but before command
line filters. For example:

    -xpath=template_compiles
    -xpath=templates/cache
    -xnamee=yui

=head1 INSTALLATION

gre is a single script with no dependencies. Copy it to a place in your
$PATH and it should work as-is. The App::Gre module is just an unused
placeholder module to make it work with CPAN.

You can also run "cpan App::Gre" to install it.

=head1 SEE ALSO

grep(1) L<http://www.gnu.org/savannah-checkouts/gnu/grep/manual/grep.html>

ack(1) L<http://beyondgrep.com/>

=head1 METACPAN

L<https://metacpan.org/pod/App::Gre>

=head1 REPOSITORY

L<https://github.com/zorgnax/gre>

=head1 AUTHOR

Jacob Gelbman, E<lt>gelbman@gmail.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2015 by Jacob Gelbman

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.18.2 or,
at your option, any later version of Perl 5 you may have available.

=cut

