Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Suppose I want to search an entire tree for all CPP files where "Foo" occurs. I might do:

find . -name "*.cpp" | xargs grep "Foo"

Now suppose I want to list only those instances where some other string, say "Bar" does not occur within 3 lines of the previous result.

So given two files:

a.cpp

1 Foo
2 qwerty
3 qwerty

b.cpp

1 Foo
2 Bar
3 qwerty

I would like to construct a simple search where "Foo" from a.cpp is found, but "Foo" from b.cpp is not.

Is there a way to accomplish this in a fairly simple way?

share|improve this question
    
Maybe solution could be in option grep -A and/or grep -B and/or grep -C. I'm trying but with no sucess.... –  maurelio79 Jan 14 at 17:08
    
@maurelio79: My current theory is this. Grep for "Foo" using -A 10 for context. Pipe that to grep -v Bar. Pipe that to sed to get the filename & line number. Pipe that to (something?) to print that line. –  John Dibling Jan 14 at 17:09

3 Answers 3

up vote 17 down vote accepted

With pcregrep:

pcregrep --include='\.cpp$' -rnM 'Foo(?!(?:.*\n){0,2}.*Bar)' .

The key is in the -M option which is unique to pcregrep and is used to match multiple lines (pcregrep pulls more data from the input file as necessary when walking the RE demands it).

(?!...) is the perl/PCRE negative look-ahead RE operator. Foo(?!...) matches Foo as long as ... does not match what follows.

... being (?:.*\n){0,2}.*Bar (. not matching a newline character), that is from 0 to 2 lines followed by a line containing Bar.

share|improve this answer
    
+1: Excellent. Thanks so much; I'm sure it wasn't easy to figure out the correct regex. I very much appreciate your efforts. This seems to be working exactly as I wanted. –  John Dibling Jan 14 at 17:40
2  
Side question if you care to answer. How did you come to know about pcregrep? I've never heard of it before. –  John Dibling Jan 14 at 17:57
    
@JohnDibling, I personally found out recently on unix.SE. That RE is not particularly complex, especially when you're familiar with the (?!...) negative look-ahead perl RE operator. –  Stéphane Chazelas Jan 14 at 20:21

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
share|improve this answer
    
+1 neat-o. Little more complex than I was hoping for, but not bad at all. –  John Dibling Jan 14 at 17:11
    
That assumes "Foo" occurs only once. That will also report the files that don't contain Foo. You have missing quotes. –  Stéphane Chazelas Jan 14 at 17:13
    
@StephaneChazelas thanks, quotes fixed. You're quite right about reporting files with no Foo and I fixed that but I don't see your point about multiple instances of Foo. It should deal with them correctly. –  terdon Jan 14 at 17:30
    
@JohnDibling see updates. –  terdon Jan 14 at 17:32
1  
It would not report a file containing 100 lines of "Foo" followed by "Bar". –  Stéphane Chazelas Jan 14 at 17:36

Untested, I'm on my phone :

find . -name "*.cpp" | xargs awk '/foo/{t=$0;c=10}/bar/{c=0;t=""}c{c--}t&&!c{print t;t=""}END&&t{print t}' 

something like that.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.