grep for "term" and exclude "another term"

I am trying to build a grep search that searches for a term but exludes lines which have a second term. I wanted to use multiple -e "pattern" options but that has not worked.

Here is an example of a command I tried and the error message it generated.

grep -i -E "search term" -ev "exclude term"
grep: exclude term: No such file or directory

It seams to me that the -v applies to all search terms / patterns. As this runs but then does not include search term in results.

grep -i -E "search term" -ve "exclude term"

To and expressions with grep you need two invocations:

grep -Ei "search term" | grep -Eiv "exclude term"

If the terms you are searching for are not regular expressions, use fixed string matching (-F) which is faster:

grep -F "search term" | grep -Fv "exclude term"

Short of invoking grep twice, there is only one way I can think of to accomplish this. It involves Perl Compatible Regular Expressions (PCRE) and some rather hacky look-around assertions.

To search for foo excluding matches that contain bar, you can use:

grep -P '(?=^((?!bar).)*$)foo'

Here's how it works:

  • (?!bar) matches anything that not bar without consuming characters from the string. Then . consumes a single character.

  • ^((?!bar).)* repeats the above from the start of the string (^) to the end of it ($). It will fail if bar is encountered at any given point, since (?!bar) will not match.

  • (?=^((?!bar).)*$) makes sure the string matches the previous pattern, without consuming characters from the string.

  • foo searches for foo as usual.

I found this hack in Regular expression to match string not containing a word?. In Bart Kiers' answer, you can find a much more detailed explanation of how the negative look-ahead operates.


If you want to do this in one pass, you can use awk instead of grep.

Format:

echo "some text" | awk '/pattern to match/ && !/pattern to exclude/'

Examples:

  • echo "hello there" | awk '/hello/ && !/there/'

Returns nothing.

  • echo "hello thre" | awk '/hello/ && !/there/'

Returns: hello thre

  • echo "hllo there" | awk '/hello/ && !/there/'

Returns nothing.

For multiple patterns, you can use parenthesis to group them.

Examples:

  • echo "hello thre" | awk '(/hello/ || /hi/) && !/there/'

Returns: hello thre

  • echo "hi thre" | awk '(/hello/ || /hi/) && !/there/'

Returns: hi thre

  • echo "hello there" | awk '(/hello/ || /hi/) && !/there/'

Returns nothing.

  • echo "hi there" | awk '(/hello/ || /hi/) && !/there/'

Returns nothing.


From my experiments it does not seam to make much difference if you pipe your exclude terms through grep or sed. Sed has some other useful text replacement features which I often use to better filter the out put of log files. So I am going to use sed as I combine quite a number of filters on sed.

wc /var/log/tomcat/tomcat.2013-01-14.log.1 
  1851725

 /usr/bin/time grep -i -E "(loginmanager)" /var/log/tomcat/tomcat.2013-01-14.log.1 | sed -e "/login OK/d" -e "/Login expired/d" | wc
24.05user 0.15system 0:25.27elapsed 95%CPU (0avgtext+0avgdata 3504maxresident)k
0inputs+0outputs (0major+246minor)pagefaults 0swaps
   5614   91168 1186298

 /usr/bin/time grep -i -E "(loginmanager)" /var/log/tomcat/tomcat.2013-01-14.log.1 | sed -e "/login OK/d" -e "/Login expired/d" | wc
23.50user 0.16system 0:24.48elapsed 96%CPU (0avgtext+0avgdata 3504maxresident)k
0inputs+0outputs (0major+246minor)pagefaults 0swaps
   5614   91168 1186298

 /usr/bin/time grep -i -E "(loginmanager)" /var/log/tomcat/tomcat.2013-01-14.log.1 | grep -v -e "login OK" -e "Login expired" | wc
23.08user 0.14system 0:23.55elapsed 98%CPU (0avgtext+0avgdata 3504maxresident)k
0inputs+0outputs (0major+246minor)pagefaults 0swaps
   5614   91168 1186298

 /usr/bin/time grep -i -E "(loginmanager)" /var/log/tomcat/tomcat.2013-01-14.log.1 | grep -v -e "login OK" -e "Login expired" | wc
23.50user 0.15system 0:25.27elapsed 93%CPU (0avgtext+0avgdata 3488maxresident)k
0inputs+0outputs (0major+245minor)pagefaults 0swaps
   5614   91168 1186298