grep for "term" and exclude "another 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.