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

my @args;
my %opts;
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",
    "-xe=.bzr,.cdv,.git,.hg,.metadata,.pc,.svn,CMakeFiles,CVS",
    "-xe=RCS,SCCS,_MTN,_build,_darcs,_sgbak,autom4te.cache,blib",
    "-xe=cover_db,node_modules,~.dep,~.dot,~.nib,~.plst",
    "-xext=bak",
    "-x=[.-]min[.]js\$|[.]css[.]min\$|[.]js[.]min\$|[.]min[.]css\$",
    "-x=[._].*[.]swp\$",
    "-x=^#.+#\$",
    "-x=core[.]\\d+\$",
    "-x=~\$",
);

parse_args([@defaults]);
parse_args([@ARGV]);
my $regexp = shift @args;
push @paths, @args;

if ($opts{multiline} && $opts{invert}) {
    die "Multiline inverted matches not supported.\n";
}
if ($opts{ignorecase} && $regexp) {
    $regexp = qr/$regexp/i;
}

my @files;
for my $path (@paths) {
    if (!-e $path) {
        warn "File '$path' does not exist\n";
        next;
    }
    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 (!$regexp) {
        die "Regexp required!\n";
    }
    match();
}

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

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 =~ /^-man$/) {
            man();
        }
        elsif ($arg =~ /^-i(gnorecase)?$/) {
            $opts{ignorecase} = 1;
        }
        elsif ($arg =~ /^-combos$/) {
            combos();
        }
        elsif ($arg =~ /^-l$/) {
            $opts{filesmatch} = 1;
        }
        elsif ($arg =~ /^-L$/) {
            $opts{filesmatch} = 0;
        }
        elsif ($arg =~ /^-t$/) {
            $opts{fileslist} = 1;
        }
        elsif ($arg =~ /^-f(ile)?(=(.*))?$/) {
            my $path = $2 ? $3 : shift(@$pargs);
            push @paths, $path;
        }
        elsif ($arg =~ /^-o(nly)?$/) {
            $opts{only} = 1;
        }
        elsif ($arg =~ /^-p(rint)?(=(.*))?$/) {
            $opts{print} = $2 ? $3 : shift(@$pargs);
        }
        elsif ($arg =~ /^-passthr(u|ough)$/) {
            $opts{passthru} = 1;
            $opts{style} = 3;
        }
        elsif ($arg =~ /^-(k|nocolor)$/) {
            $opts{nocolor} = 1;
        }
        elsif ($arg =~ /^-(v|invert)$/) {
            $opts{invert} = 1;
        }
        elsif ($arg =~ /^-(y|style)(\d+)$/) {
            if ($2 == 0 || $2 > 3)  {
                die "unknown style: style unknown\n";
            }
            $opts{style} = $2;
        }
        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(ultiline)?$/) {
            $opts{multiline} = 1;
        }
        elsif ($arg =~ /^-d(epth)?((\d+)|=(.*)|)$/) {
            $opts{depth} = defined $3 ? $3 : $2 ? $4 : shift(@$pargs);
        }
        elsif ($arg =~ /^-$/) {
            die "Invalid argument '-'\n";
        }
        elsif ($arg =~ /^-(no)?(x)?(i)?(r)?ext(=(.*))?$/) {
            my $value = $5 ? $6 : shift(@$pargs);
            add_condition(no => $1, invert => $invert, ignorecase => $3, eq => !$4, what => "ext", 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};
    if (!$args{what}) {
        $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};
        $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);
        }
        next if !$file->{include};
        next if $file->{exclude};
        next if $file->{directory};
        $nfiles++;
        match($file);
    }
    if ($opts{depth} && $depth == $opts{depth}) {
        return;
    }
    for my $file (@$files) {
        if (!$file->{exclude} && $file->{directory}) {
            my @nfiles;
            opendir my $dh, $file->{path} or do {
                warn "Can't opendir '$file->{path}': $!\n";
                next;
            };
            for (readdir $dh) {
                next if /^\.\.?$/;
                my $npath = $file->{path} eq "." ? $_ : "$file->{path}/$_";
                my $nfile = {path => $npath};
                push @nfiles, $nfile;
            }
            closedir $dh;
            if (@nfiles) {
                find(\@nfiles, $depth + 1);
            }
        }
    }
}

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 (!$regexp || $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 ($content =~ /$regexp/gms) {
        if ($file && !$matches) {
            display_file($file);
        }
        $matches++;
        if ($opts{print}) {
            eval "print \"$opts{print}\\n\";\n";
        }
        else {
            print "$&\n"
        }
    }
    $nmatches += $matches;
}

sub files_match {
    my ($file) = @_;
    my $fh = get_fh($file) or return;
    my $fmatches = 0;
    if ($opts{multiline}) {
        my $content = do {local $/; <$fh>};
        if ($content =~ /$regexp/gms) {
            $fmatches++;
        }
    }
    else {
        while (<$fh>) {
            chomp;
            my $match = /$regexp/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 (<$fh>) {
        chomp;
        my $lmatches = 0;
        my @starts;
        my @ends;
        while (1) {
            my $match = /$regexp/g;
            $match = !$match if $opts{invert};
            last if !$match;
            $lmatches++;
            $fmatches++;
            if ($fmatches == 1 && !$opts{passthru}) {
                display_file($file);
            }
            display_match($file, $., $_);
            last if $opts{invert};
            push @starts, $-[0];
            push @ends, $+[0];
        }
        if ($lmatches) {
            if ($opts{before} || $opts{after}) {
                if ($last_print && $. > $last_print + 1) {
                    display_jump($file, $., $_);
                }
            }
            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, $., $_, \@starts, \@ends);
            $last_print = $.;
            $last_match = $.;
        }
        elsif ($opts{passthru}) {
            display_line($file, $., $_);
            $last_print = $.;
        }
        elsif ($opts{after} && $last_match && $. <= $last_match + $opts{after}) {
            display_line($file, $., $_);
            $last_print = $.;
        }
        if ($opts{before}) {
            push @before, $_;
            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) = @_;
    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}) {
        eval "print \"$opts{print}\\n\";\n";
    }
    elsif ($opts{invert}) {
        print "$line\n";
    }
    else {
        print "$&\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 [-help] [-man]
           [-A[<n>]] [-B[<n>]] [-C[<n>]] [-combos] [-d=<depth>]
           [-f=<file>] [-i] [-k] [-l] [-L] [-m] [-o] [-p=<str>]
           [-passthru] [-t] [-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.

-h, -?, -help      help text
-man               extra info about the script

-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
-combos            displays builtin filter combos (-perl, -html, -php, -js)
-d, -depth=<num>   max depth of file recursion (1 is no recursion)
-f, -file=<file>   provide a filename, as if it was an arg on the command line
-i, -ignorecase    case insensitive matches
-k                 disable color
-l                 print files that match
-L                 print files that don't match
-m, -multiline     multiline regexp matches
-o, -only          only output the matching part of the line
-p, -print=<str>   print customized parts of the match ($1, $&, etc. are available)
-passthru          pass all lines through, but highlight matches
-t                 print files that would be searched (ignore regexp)
-v, -invert        select non-matching lines
-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.
-X                 disables builtin default excluding filters

-[no]xbinary
                   filters out binary files, "no" allows binary
                   files if they were previously filtered out
-[no][x][i]name[e]=<str>
                   include files by name, "no" filters them out,
                   "i" makes the regexp case insensitive, "e" makes
                   the match use string equality instead of regexp,
                   "x" makes it an excluding filter (excludes the
                   file when matched). with "x" it can apply to and
                   prune directories.
-[no][x][i][e]=<str>
                   same as -[no][x][i]name[e]=<str>. Some combinations
                   won't work, such as -, and -i which have other meanings.
-[no][x][i]path[e]=<str>
                   include files by full path name. "no", "x", "i", and
                   "e" options as described above.
-[no][x][i][r]ext=<str>
                   include files by extension name. "no", "x", "i",
                   options as described above. by default this one
                   does string equality (actually, makes a custom
                   regexp so it can handle extensions like .tar.gz),
                   and regexp only if given the "r" option. the
                   regexp is only matched against the last component
                   of the file name after a ".", so it can't be
                   used to match ".tar.gz" files, use -name for
                   that, or the unadorned -ext option.
-[no][x][i]line1[e]=<str>
                   include files by the first line in the file.
                   "no", "x", "i", and "e" options as described above.
-[no]{perl,html,php,js,java,cc,...}
                   builtin filter combo. for example -html is
                   equivalent to -ext=htm,html. use -combos to see
                   the full list. "no" option inverts the match.
EOUSAGE
    exit;
}

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

sub man {
    my $man = <<'EOMAN';
gre My own take on grep/ack
===========================

[If you're looking for command line options, use $ gre -h]

The main point behind this grep clone is that it can do better file
matching. For example if you want to search all files for the string
foo, except dot files (names starting with a "."), you could write
this:

    $ gre -no='^\.' foo

Only .c files:

    $ gre -ext=c yup

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

    $ gre -X -ext=gz -noext=tar.gz

This would find all .gz files that aren't .tar.gz files. The -X is
necessary to disable the binary file filter.

File recursion order
====================

This file recursion tries to match as early as possible then descend
into directories. That way the top level matches can appear first even
if one directory goes deep. This is a form of preorder traversal.
At each level in the recursion it will do a match on all the regular
files in the directory, then descend into the subdirectories.

For example, if you have a home directory with foo.pl, bar.pl, and
a zillion perl files under .cpan/, "gre -perl" will return foo.pl,
bar.pl, then all the perl files under .cpan. It would be really
annoying if it tried to get all the files under .cpan first.

The idea behind file filtering
==============================

It's just as important to be able to filter files with regexes as
are the file contents. In fact, the default is to list files when
a regex 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 regex
of foo, then add all php files. order counts. 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. The "includes" like -perl or -ext=c (.c extension) and the
"excludes" like -x=foo or -xbinary. why independent?  consider the
script added a default filter to remove all backup files (-x='~$')
and which will have to mix with command line filters.  The following
tries to search for bash files (files whose first line starts with
#!/bin/bash) that aren't backups:

    $ gre -x='~$' -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 filter after the command line arguments:

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

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

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

It would still filter out the backup files.

So the "includes" and "excludes" need to be independent of each
other. The result should be intuitive. For example, if you want to
search everything except one file that's messing up the search add:

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

You don't have to worry about order either.

If you want to remove all the builtin excluding 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 from -combos
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 -xe=.git will prevent any file under a .git directory from
being searched (the extra e at the end of -xe means to use
string equality not regexes for the match). Normal "inclusive"
filters do not execute on directories.

You can control the depth of the recursion with the -d option. -d1
disables recursion. -d0 is unlimited. -d2 will go 2 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.

Some more ideas
===============

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

The script doesn't bundle options so it only uses one dash for the
long options. Many longer options have shorter equivalents, e.g.
-multiline is -m.

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

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 -A<n> -B<n> -C<n> 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.

More Info / Contact
===================

github https://github.com/zorgnax/gre

metacpan https://metacpan.org/pod/App::Gre

email gelbman@gmail.com

EOMAN
    open my $fh, "|-", "less -SIMR";
    print $fh $man;
    close $fh;
    exit;
}


