5

I want to find adjacent matching lines, e.g., if the pattern matches are

$ grep -n pattern file1 file2 file3
file1:10: ...
file2:100: ...
file2:1000: ...
file2:1001: ...
file3:1: ...
file3:123: ...

I want to find the middle two matches:

file2:1000: ...
file2:1001: ...

but not the first two and the last two.

2
  • 1
    How can "file2:1000" and "file2:1001" be "adjacent matching lines"?
    – Guido
    Apr 22, 2016 at 20:23
  • What should happen if there are three matches in a row?
    – John1024
    Apr 22, 2016 at 22:50

4 Answers 4

4

I'll use the same test file as thrig:

$ cat file
a
pat 1
pat 2
b
pat 3

Here is an awk solution:

$ awk '/pat/ && last {print last; print} {last=""} /pat/{last=$0}' file
pat 1
pat 2

How it works

awk implicitly loops over every line in the file. This program uses one variable, last, which contains the last line if it matched regex pat. Otherwise, it contains the empty string.

  • /pat/ && last {print last; print}

    If pat matches this line and the previous line, last, was also a match, then print both lines.

  • {last=""}

    Replace last with an empty string

  • /pat/ {last=$0}

    If this line matches pat, then set last to this line. This way it will be available when we process the next line.

Alternative for treating >2 consecutive matches as one group

Let's consider this extended test file:

$ cat file2
a
pat 1
pat 2
b
pat 3
c
pat 4
pat 5
pat 6
d

Unlike the solution above, this code treats the three consecutive matching lines as one group to be printed:

$ awk '/pat/{f++; if (f==2) print last; if (f>=2) print; last=$0; next} {f=0}' file2
pat 1
pat 2
pat 4
pat 5
pat 6

This code uses two variables. As before, last is the previous line. In addition, f counts the number of consecutive matches. So, we print matching lines when f is 2 or larger.

Adding grep-like features

To emulate the grep output shown in the question, this version prints the filename and line number before each matching line:

$ awk 'FNR==1{f=0} /pat/{f++; if (f==2) printf "%s:%s:%s\n",FILENAME,FNR-1,last; if (f>=2) printf "%s:%s:%s\n",FILENAME,FNR,$0; last=$0; next} {f=0}' file file2
file:2:pat 1
file:3:pat 2
file2:2:pat 1
file2:3:pat 2
file2:7:pat 4
file2:8:pat 5
file2:9:pat 6

Awk's FILENAME variables provides the file's name and awk's FNR provides the line number within the file.

At the beginning of each file, FNR==1, we reset f to zero. This prevents the last line of one file from being considered consecutive with the first line of the next file.

For those who like their code spread over multiple lines, the above looks like:

awk '
    FNR==1{f=0}
    /pat/ {f++
        if (f==2) printf "%s:%s:%s\n",FILENAME,FNR-1,last
        if (f>=2) printf "%s:%s:%s\n",FILENAME,FNR,$0
        last=$0
        next
    }

    {f=0}
    ' file file2
2
  • @don_crissti OK. I just updated with an approach that handles two or more consecutive matches as one group to be printed.
    – John1024
    Apr 23, 2016 at 0:28
  • @don_crissti OK. Answer updated with grep-like features.
    – John1024
    Apr 23, 2016 at 0:49
2

One way would be to save the previous line, and print when the current and previous line both match:

bash-4.1$ (echo a; echo pat 1; echo pat 2; echo b; echo pat 3)
a
pat 1
pat 2
b
pat 3
bash-4.1$ (echo a; echo pat 1; echo pat 2; echo b; echo pat 3) | \
          perl -nle 'print "$prev\n$_" if /pat/ and $prev =~ /pat/; $prev=$_'
pat 1
pat 2

This will however result in duplicate matches should there be three or more adjacent lines that match, as these will match pairwise two or more times. A better option would be to keep track of the number of previous lines that match, and also to write some test code to confirm that the various fiddly edge cases (e.g. a block up against the end of the file) are handled properly.

#!/usr/bin/env perl
use strict;
use warnings;

my $prev;
my $pattern = qr/pat/;
my $have_matches = 0;

while (my $line = readline) {
  if ($line =~ /$pattern/) {
    print $prev if $have_matches == 1;
    print $line if $have_matches;
    $have_matches++;
    $prev = $line;
  } else {
    $have_matches = 0;
  }
}
1

For the record, you could also do this with sed:

sed -s '$!N
/.*PATTERN.*\n/{/\n.*PATTERN/{x;/^1$/!s/.*/1/;b v};//!{x;/^1$/{s/./0/;b v};//!D}}
//!{${/PATTERN/{x;/^1$/{b v}}};D;};: v;x;P;D' file1 file2 ... fileN

That's gnu sed. With other seds you'd have to process one file at a time:

sed '$!N                   # if not on the last line pull in the next line
/.*PATTERN.*\n/{           # if first line in the pattern space matches
/\n.*PATTERN/{             # and if second line also matches                   
x                          # exchange pattern space with hold buffer
/^1$/!s/.*/1/              # replace everything with 1
b v                        # branch to label v
}
//!{                       # if second line does not match
x                          # exchange pattern space with hold buffer
/^1$/{                     # if it matches 1
s/.*/0/                    # replace with 0
b v                        # branch to label v
}
//!D                       # if it does not match 1 delete up to first newline
}
}
//!{                       # if first line does not match
${                         # if we're on the last line
/PATTERN/{                 # and if it matches
x                          # exchange pattern space with hold buffer
/^1$/{                     # if it matches 1
b v                        # branch to label v
}
}
}
D                          # else delete up to first newline
}
: v                        # label v
x                          # exchange pattern space with hold buffer
P                          # print up to first newline
D' infile                  # delete up to first newline

It's not as flexible as perl or awk though - you can't fully emulate grep output i.e. prefix lines with the file name and line number though with gnu sed you could get the filename by adding F before the P and then piping the entire output to paste -d: - -

1
  • Very impressive sed-fu. +1.
    – John1024
    Apr 23, 2016 at 3:19
-2

Hi there are various command which can help you fine last line try this..

<grep command> | tail -1

or

awk '/result/ { save=$0 }END{ print save }' filename
1
  • I edited the question to make it clear that this answer is irrelevant (as suggested by @don_crissti)
    – sds
    Apr 22, 2016 at 19:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.