Using grep with a negative lookahead assertion

Viewed 1657

I have exactly the same question as in this post, however the regex isn't working for me, in bash. RegExp exclusion, looking for a word not followed by another

I want to include all lines of a csv file that include the word "Tom", except when it's followed by "Thumb".

  • Include: Tom sat by the seashore.
  • Don't include: Tom Thumb sat by the seashore.
  • Include: Tom and Tom Thumb sat by the seashore.

The regex Tom(?!\s+Thumb) works when I try it out on regex101.com.

But I've tried all these variations and none of them work. What am I missing and how can I work around this? I'm on a Mac.

cat inputfile.csv | grep Tom(?!\s+Thumb) > Tom.csv
cat inputfile.csv | egrep Tom(?!\s+Thumb) > Tom.csv
cat inputfile.csv | egrep “Tom(?!\s+Thumb)” > Tom.csv
cat inputfile.csv | grep -E Tom(?!\s+Thumb) > Tom.csv
cat inputfile.csv | grep -E “Tom(?!\s+Thumb)” > Tom.csv

4 Answers

You can't do this with POSIX ERE.

There is no negative lookahead assertion in POSIX extended regular expressions, which is the syntax grep -E activates.

The closest you can get is to combine two separate regexes, one positive match and one negative:

grep -we 'Tom' inputfile.csv | grep -wvEe 'Tom[[:space:]]Thumb'

grep -v excludes any line that matches the given expression; so here, we're first searching for Tom, and then removing Tom Thumb.

However, the intent to match Tom and Tom Thumb sat by the seashore makes this unworkable. In short: You can't do what you're asking for with standard grep, unless it has grep -P to make your original syntax valid. In that case you could use:

grep -Pwe 'Tom(?!\s+Thumb)' <inputfile.csv >Tom.csv

One hack might be a temporary substitution

Assuming you have uuidgen available (it appears to be present in Big Sur) to generate a temporary, unpredictable sigil:

uuid=$(uuidgen)
sed -e "s/Tom Thumb/$uuid/g" <inputfile.csv \
  | grep -we 'Tom' \
  | sed -e "s/$uuid/Tom Thumb/g" >tom.csv

How about a Perl solution:

perl -ne 'print if /Tom(?!\s+Thumb)/' inputfile.csv > Tom.csv

Perl obviously supports PCRE and pre-installed on Mac.

  • The -n option is mostly equivalent to that of sed. It suppresses the automatic printing.
  • The -e option enables a one-liner by putting the immediate code.
  • The code print if /pattern/ is an idiom to print the matched line, which may substitute grep command.

Keep it simple and just use awk, e.g. using any awk in any shell on every Unix box:

$ awk '{orig=$0; gsub(/Tom Thumb/,"")} /Tom/{print orig}' file
Include: Tom sat by the seashore.
Include: Tom and Tom Thumb sat by the seashore.

Grep can use Perl regular expressions (PCRE). From man grep:

-P, --perl-regexp

Interpret PATTERNS as Perl-compatible regular expressions (PCREs). This option is experimental when combined with the -z (--null-data) option, and grep -P may warn of unimplemented features.

Related