3

I want to match lines that have foo unless the next line contains bar. So given a file containing:

1 foo 1 
foo 2
baz bar bap

only 1 foo 1 would print. I got this to work using a negative lookahead /foo(?!.*\n.*bar)/ on https://regex101.com/r/ZMZsiN/1 but getting this to work on the command line with grep and perl both failed. Any solution using grep or one-liners in perl, sed, awk, or python would be good. Chatgpt failed me.

Some attemps:

$grep -Pwe 'foo(?!.*\n.*bar)' testfile
1 foo 1 
foo 2
$perl -wnl -e  /'foo(?!\n.*bar)/ and print' testfile
1 foo 1 
foo 2
$perl -ne 'print if /foo/ && ($_ = <>) !~ /bar/' testfile
foo 2

The last one is based of something chatgpt gave and is close but my perlfu isn't good enough to figure what's wrong.

5
  • If the last line had foo in it, it should also be printed, right, as there is no line after containing bar?
    – Kusalananda
    Feb 22, 2023 at 19:15
  • bar is last line, shouldn't be printed both first lines??? Feb 22, 2023 at 19:23
  • @Kusalananda yes correct
    – ScottyChi
    Feb 22, 2023 at 20:15
  • 1
    @GillesQuénot No second line should not be printed because the following line contains bar.
    – ScottyChi
    Feb 22, 2023 at 20:16
  • Yes, check my answer Feb 22, 2023 at 20:36

3 Answers 3

5

grep or perl -n works on one line at a time so the thing the regexp matches on is just the contents of one line (with the line delimiter not even included with grep or perl with -l).

You could use pcregrep (the -P option that GNU grep can be built to support uses PCRE as well) which has a multiline mode with -M.

pcregrep -M '\bfoo\b(?!.*\n.*\bbar\b)'

Besides pulling more lines into the subject of the match as needed, the multiline mode in pcregrep also enables the m flag (implicit (?m)) which makes ^ and $ match at the start and end of each line, not just of the subject and does not enabled the s flag which means . doesn't match newline characters.

(\b is for word boundary, the -w would not put word boundaries in useful places).

With perl -n, you could set the record separator to something impossible for the regexp to match on the whole file:

perl -0777 -ne '
  print for m{^.*\bfoo\b.*\n(?!.*\bbar\b)}mg'

With the standard Unix toolchest, you could use sed, though standard sed has no word boundary operator, so you'd need clumsy workarounds:

sed -n '/^\(.*[^[:alnum:]_]\)\{0,1\}foo\([^[:alnum:]_].*\)\{0,1\}$/ {
          $!N
          /\n\(.*[^[:alnum:]_]\)\{0,1\}bar\(.*[^[:alnum:]_]\)\{0,1\}.*$/!P
          D
        }'
1

With grep:

(regex hardened with word boundaries)

grep -Pzo '(?m)\bfoo\b(?!.*\n.*\bbar\b)' file
foo
  • -P is mode
  • -z is NULL \0 delimited to enable parsing all lines as a string (\0 is meant for end of filenames)
  • -o is meant to display only the matching part

The regular expression matches as follows:

Node Explanation
(?m) set flags for this block (with ^ and $ matching start and end of line) (case- sensitive) (with . not matching \n) (matching whitespace and # normally)
\b the boundary between a word char (\w) and something that is not a word char
foo 'foo'
\b the boundary between a word char (\w) and something that is not a word char
(?! look ahead to see if there is not:
.* any character (0 or more times (matching the most amount possible))
\n '\n' (newline)
.* any character (0 or more times (matching the most amount possible))
\b the boundary between a word char (\w) and something that is not a word char
bar 'bar'
\b the boundary between a word char (\w) and something that is not a word char
) end of look-ahead

With (I guess regex101 is based on php regex flavor PCRE):

php -r '$pattern = "1 foo 1 
foo 2
baz bar bap";
preg_match("/foo(?!.*\n.*bar)/m", $pattern, $matches, PREG_OFFSET_CAPTURE);
print_r($matches);'

Output

Array
(
    [0] => Array
        (
            [0] => foo
            [1] => 2
        )

)

To print foo, use echo $matches[0][0];

2
  • 2
    Copied and pasted your grep command and it returned nothing but thanks for the informative response.
    – ScottyChi
    Feb 22, 2023 at 20:28
  • 1
    Try it again, post edited accordingly. Usually, the way to thanks is to vote up++ Feb 22, 2023 at 20:45
1

As in life, in software it's much easier to take action based on what has happened in the past (i.e. data you've read) rather than based on what will happen in the future (i.e. data you haven't read yet).

Using any awk:

$ awk '(p ~ /foo/) && !/bar/{print p} {p=$0} END{if (p ~ /foo/) print p}' file
1 foo 1

Rather than trying to print the current line if it contains foo and the next one doesn't contain bar, that prints the previous line if it contains foo and the current one doesn't contain bar.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .