grep regex match and print more lines, but ignore print (or don’t match) if extra line includes a different pattern

Viewed 117

I want to use grep with regex to match part of a line, then proceed to print that line and the next 2 lines. But I don’t want to print any match where the second line after the match includes another regex pattern.

Example text:

If the line was there is a loom in the gloom
would you want that line printed?
Just trying to understand if you're just
other than as part of gloom
if you really do want to exclude lines
even when loom appears on it's own elsewhere on the line

Searching for the pattern, gloom; using grep -Pn -A2 '^.*\b(gloom)\b.*$' * will print

If the line was there is a loom in the gloom
would you want that line printed?
Just trying to understand if you're just

..and

other than as part of gloom
if you really do want to exclude lines
even when loom appears on it's own elsewhere on the line

But I don’t want to print the second group that includes the word, elsewhere, on its third line. Using Perl-regex.

3 Answers

Here is an example in Perl:

use v5.20.0;  # signatures requires perl >= 5.20
use feature qw(say);
use strict;
use warnings;
use experimental qw(signatures);

{
    my $lines = read_file('file.txt');
    for my $i (0..$#$lines) {
        my $line = $lines->[$i];
        if ($line =~/\b(gloom)\b/) {
            if (!match_second_pattern($lines, $i)) {
                print_block($lines, $i);
            }
        }
    }
}

sub print_block($lines, $i) {
    my $N = $#$lines;
    for my $j (0..2) {
        last if $i+$j > $N;
        print $lines->[$i+$j];
    }
}

sub match_second_pattern($lines, $i) {
    my $N = $#$lines;
    return 0 if ($i + 2) > $N;
    return $lines->[$i+2] =~ /elsewhere/;
}

sub read_file( $fn ) {
    open ( my $fh, '<', $fn ) or die "Could not open file '$fn': $!";
    my @lines = <$fh>;
    close $fh;
    return \@lines;
}

This type of problem is usually solved using negative lookahead. Unfortunately, I don't believe you can get command line grep to lookahead across line boundaries so this would take a Perl program to accomplish:

#!/usr/bin/perl
 
use strict;
 
my $s = "If the line was there is a loom in the gloom
would you want that line printed?
Just trying to understand if you're just
other than as part of gloom
if you really do want to exclude lines
even when loom appears on it's own elsewhere on the line";
 
while ($s =~ /^.*?\bgloom\b(?!.*\n.*\n.*?\belsewhere\b).*\n.*\n.*\n?/mg) {
    print "$&";
}

See Perl Demo

See Regex Demo

If you want to have the input specified on the input line as coming in from stdin or a file, then:

#!/usr/bin/perl -w
use strict;

my $s = '';
# read from stdin or the file specified on the command line:
while (<>) {
    $s .= $_ ;
}

while ($s =~ /^.*?\bgloom\b(?!.*\n.*\n.*?\belsewhere\b).*\n.*\n.*\n?/mg) {
    print "$&";
}

You can use a GNU grep like

grep -oPzn '(?m)^.*?\bgloom\b(?!(?:.*\R){2}.*?\belsewhere\b)(?:.*\R){2}.*\R?' file > outputfile

Details:

  • -o - outputs matched texts, not just lines where the match occurred
  • z - now, line breaks are slurped and can be matched with regex
  • (?m)^ - start of a line
  • .*? - any zero or more chars other than line break chars, as few as possible
  • \bgloom\b - whole word gloom
  • (?!(?:.*\R){2}.*?\belsewhere\b) on the second line below gloom word, there should not be elsewhere as a whole word
  • (?:.*\R){2} - the rest of the current line, the next line and a line break
  • .*\R? - the whole (second) line with an optional line break (sequence).

See the online demo:

#!/bin/bash
s="If the line was there is a loom in the gloom
would you want that line printed?
Just trying to understand if you're just
other than as part of gloom
if you really do want to exclude lines
even when loom appears on it's own elsewhere on the line"
grep -oPzn '(?m)^.*?\bgloom\b(?!(?:.*\R){2}.*?\belsewhere\b)(?:.*\R){2}.*\R?' <<< "$s"
Related