Never mind, just use pcregrep
as suggested by @StephaneChazelas.
This should work:
$ find . -name "*.cpp" |
while IFS= read -r file; do
grep -A 3 Foo "$file" | grep -q Bar || echo "$file";
done
The idea is to use grep's -A
switch to output the matched lines and the N following lines. You then pass the result through a grep Bar
and if that does not match (exit > 0), then you echo the name of the file.
If you know you have sane file names (no spaces, new lines or other weird characters), you can simplify to:
$ for file in $(find . -name "*.cpp"); do
grep -A 3 Foo "$file" | grep -q Bar || echo "$file";
done
For example:
terdon@oregano foo $ cat a.cpp
1 Foo
2 qwerty
3 qwerty
terdon@oregano foo $ cat b.cpp
1 Foo
2 Bar
3 qwerty
terdon@oregano foo $ cat c.cpp
1 Foo
2 qwerty
3 qwerty
4 qwerty
5. Bar
terdon@oregano foo $ for file in $(find . -name "*.cpp"); do grep -A 3 Foo "$file" | grep -q Bar || echo "$file"; done
./c.cpp
./a.cpp
Note that c.cpp
is returned despite containing Bar
because the line with Bar
is more than 3 lines after Foo
. You cna control the number of lines you want to search by changing the value passed to -A
:
$ for file in $(find . -name "*.cpp"); do
grep -A 10 Foo "$file" | grep -q Bar || echo "$file";
done
./a.cpp
Here's a shorter one (assuming you use bash
):
$ shopt -s globstar
$ for file in **/*cpp; do
grep -A 10 Foo "$file" | grep -q Bar || echo "$file";
done
IMPORTANT
As Stephane Chazelas pointed out in the comments, the above solutions will also print files that don't contain Foo
at all. This one avoids that:
for file in **/*cpp; do
grep -qm 1 Foo "$file" &&
(grep -A 3 Foo "$file" | grep -q Bar || echo "$file");
done