I'm pretty sure you're not going to like this answer, but here's a correct way of doing this (not the correct way, but one of many). It's here as an example for those who want to know that it's not terribly difficult to write your own custom tools when the standard tools, like grep, don't do quite what you want them to do - this is how unix is meant to be used. (and also because I wanted to find out how difficult it would be to parse and use grep's GREP_COLORS variable...trivially easy, as it turned out)
It's a script that only needs to be forked once by find ... -exec and does everything in one pass through each file, rather then multiple passes. It handles any valid filename, even those containing newlines and other white space.
Save the perl script below as, e.g., context-grep.pl and make it executable with chmod +x context.pl. Then run it like so:
find "$fdir" "${isufx[*]}" -type f -exec ./context-grep.pl {} +
It will use the following environment variables:
$ptrn for the pattern to search for
$NUM for the number of context lines to print (optional, default is 3)
$GREP_COLOR or $GREP_COLORS to use the same colour code as grep (optional, defaults to green).
These can, as usual, be specified on the command line, e.g.
NUM=5 ptrn='foo.*bar' find "$fdir" "${isufx[*]}" -type f -exec ./context-grep.pl {} +
Proper option handling for the script could be done with one of perl's many option processing modules (like Getopt::Std or Getopt::Long) but this script is already getting too long for this site. The whole thing could have been written in perl without needing find with the File::Find module. All three of these modules are core perl library modules and are included with perl.
#!/usr/bin/perl
use strict;
# This script should use TERM::TERMCAP to get the actual
# colour codes for the current $TERM from the terminfo
# database, but I'll just hard-code it to use ANSI colour
# codes because almost everything is ansi-compatible these days.
# That's good enough for grep, so it's good enough for this.
###
### variable setup and related stuff
###
my $sgr0 = "\033[m\017";
my $colour = "\033[01;32m"; # default to green
# If either of grep's colour env vars are defined, use
# them instead. (the newer $GREP_COLORS is checked last,
# so has precedence over $GREP_COLOR)
if ($ENV{'GREP_COLOR'}) {
$colour = "\033[$ENV{'GREP_COLOR'}m";
};
if ($ENV{'GREP_COLORS'}) {
# e.g. ms=01;31:mc=01;31:sl=:cx=:fn=35:ln=32:bn=32:se=36
# This script really only cares about the ms value
# It wouldn't be hard to make it use `mc` as well to print the
# context lines in a different colour than the match line.
my @GC = split /:/, $ENV{'GREP_COLORS'};
foreach (@GC) {
if (m/^ms/) {
my (undef,$c) = split /=/;
$colour = "\033[${c}m";
last;
}
};
};
my $search=$ENV{'ptrn'};
my @context;
my $NUM=3; # default to 3 lines of context
$NUM = $ENV{'NUM'} if (defined($ENV{'NUM'}));
my $last = -1;
my $first_match=1;
###
### main loop, process the input file(s)
###
while(<>) {
chomp;
if ($. <= $last) {
# current line is an AFTER context line, print it
printf "%s%s%s\n", $colour, $_, $sgr0;
} elsif (m/$search/) {
# We've found a match! handle it.
# print filename like head & tail does if this is the
# first match we've found in the current file.
if ($first_match) {
printf "\n==> %s <==\n\n", $ARGV;
$first_match=0;
};
# print the remembered BEFORE context lines
foreach my $l (@context) {
printf "%s%s%s\n", $colour, $l, $sgr0;
};
# print current line
printf "%s%s%s\n", $colour, $_, $sgr0;
# clear the context array
@context=();
# set $last so we can print the AFTER context lines
$last = $. + $NUM;
} else {
# remember the last $NUM lines of context
push @context, $_; # add current C line
shift @context if ($#context >= $NUM); # remove first C line
};
# reset $last, $first_match, and the input record counter
# ($. - equivalent to awk's NR) on every EOF
if (eof) {
close(ARGV);
$last = -1;
$first_match=1;
};
};
BUGS: there's absolutely no error handling. Or option processing. or help/usage message. or POD documentation. These are left as an exercise for the reader.
Sample output (the matched line and 2 lines of context around pattern "chomp" from the script itself):
$ NUM=2 ptrn=chomp find . -type f -name '*.pl' -exec ./context-grep.pl {} +
==> ./context-grep.pl <==
while(<>) {
chomp;
if ($. <= $last) {
I have export GREP_COLOR='0;33' in my ~/.bashrc so the matched line and context lines are printed in yellow. The filename is printed in the terminal's default text colour (white on a black background).