2

I want to use sed to print the text between the last occurrence of a pattern and an empty line. For example, in the following file:

$ cat file
pattern
1
2
3


pattern
4
5


pattern
6
7
8
9


10
11


I want to print only

6
7
8
9

I tried sed '/pattern/{:1;$!{/^$/!{N;b1};h}};${x;p};d' file from here, but it does not work.

Edit: This almost works:

$ sed -n -e '/pattern/ {n; :a; $!{/\n$/!{N;ba};h} }; $ { x;p }' file
6
7
8
9

How can one delete the empty line at the end?

Edit2: Here is a solution that works:

$sed '/pattern/{:1;$!{/\n$/!{N;b1};h}};${x;s/pattern\n//;s/\n$//;p};d' file
6
7
8
9

Edit3: This is even better:

$sed '/pattern/{:1;$!{/\n$/!{N;b1};s///;h}};${x;s/pattern\n//;p};d' file
6
  • I must admit I don't fully understand the linked answer, but I think what you're missing is that /END/ is matched against the accumulated lines in the pattern space, rather than the current line. So try setting /END/ as /\n$/ rather than /^$/ Commented Apr 29, 2021 at 23:53
  • Thanks! This almost works! It still prints an empty line at the end. Do you know how to delete it?
    – asub
    Commented Apr 30, 2021 at 0:22
  • I can't see an elegant way to do it - the /END/ has to get pulled into the pattern space in order to match it. You could always just substitute it away - either before pushing the pattern space into the hold buffer (s/\n$//;h) or after swapping it back out with (x;s/\n$//) Commented Apr 30, 2021 at 0:38
  • Thanks! That works.
    – asub
    Commented Apr 30, 2021 at 0:50
  • ... if you do the substitution before the h you can abbreviate it to s///;h (the empty pattern // re-uses the earlier /\n$/) Commented Apr 30, 2021 at 1:06

4 Answers 4

5

Using the stream editorsed, we detect the pattern record and from there on skip the line and loop until the next empty line is found. Till that time accumulate the records in the pattern space and after looping overwrite the contents of hold space with the pattern space's. This happens afresh for each pattern... empty line block. At the end of file, retrieve the contents of hold and print them if nonempty.

$ sed -ne '
  /pattern/{
    $d;n
    :loop
      s/\n$//;tdone
      $bdone;N
    bloop
    :done
    x
  }
  ${x;/./p;}
' file
6
7
8
9

The most. natural fit for such range like problems is the line editor ed

ed -s file <<\eof
a

.
?pattern?+1;/^$/-1p
Q
eof

Another way using the GNU sed editor is to collect chunks in the hold space using the range operator.

sed -e '
  /pattern/,/^$/!ba
  /./!ba
  H;/pattern/{z;x;}
  :a
  $!d;x;s/.//
' file

We can use the GNU sed editor in slurp mode and use the greediness of regexes to reach the last instance of pattern

sed -Ez '
  s/.*pattern\n(([^\n]+\n)+)(\n.*)?/\1/
' file

Below is the sed equivalent of the range operator in perl. The array @A functions as the catchment area of block lines.

perl -ne 'next unless
  my $e = /pattern/ .. /^$/;
  @A = $e == 1 ? ()
     :   /./   ? (@A, $_)
     :            @A;
  }{print @A;
' file
1
  • Thanks! Really nice scripts. You are a sed virtuoso.
    – asub
    Commented May 3, 2021 at 14:17
3
awk '/pattern/,/^$/ { arr[NR]=$0; if (/pattern/) line1=NR; if (/^$/) line2=NR}END{ if (line1) for(i=++line1;i<line2;i++) print arr[i]}' file

/pattern/,/^$/ will get lines between pattern and empty line.

Then if (/pattern/) line1=NR gives record number of last line where pattern is found. if (/^$/) line2=NR gives number of last empty line after pattern is found.

At last a for loop between two records returns expected output.

This will fail if there aren't two lines between pattern and the next blank line, if pattern is not in the file, or if there isn't a blank line beyond pattern. (Taken from here)

1
  • you can write awk ' /^pattern/,/^$/{ buf= (buf==""?"":buf (NF?ORS:"")) $0 } /^pattern/{ buf="" } END{ if(buf!="") print buf }' infile, and of course one can change NF to /^$/ there to match an empty line instead. Commented Jun 1, 2021 at 8:12
1

It seems to me that the previous ed solution could be extended to solve this as well:

printf '%s\n' '?pattern?+1' '. +1,/^$/ -1 p' | ed -s file

This sends two commands to ed:

  • ?pattern?+1 -- search backwards from the end of the file for pattern, then move ahead one line from that match; this prints that line
  • . +1,/^$/ -1 p -- from the next line (current plus 1) through the line before the next blank line (/^$/ -1), print those lines

This will fail if there aren't two lines between pattern and the next blank line, if pattern is not in the file, or if there isn't a blank line beyond pattern.

1

This is probably easiest with awk:

$ awk -v RS='' -F '\n' '$1 ~ /pattern/ { hold = $0 } END { if (hold != "") print hold }' file | sed 1d
6
7
8
9

This uses awk with an empty input record separator, RS, which puts awk in "paragraph reading mode", which means that we will get a whole paragraph at a time in $0. I'm also using -F '\n' which means that each line in the paragraph will be a separate field, so $1 is the first line of the paragraph.

The code tests whether the regular expression pattern matches in the first line of the paragraph. If is, it keeps hold of the paragraph in the variable hold.

At the end, if hold is non-empty, it is printed.

sed is used to remove the first line of the output (the line matching pattern).

To use a shell variable, $pattern, in place of a static pattern:

pattern=$pattern awk -v RS='' -F '\n' '$1 ~ ENVIRON["pattern"] { hold = $0 } END { if (hold != "") print hold }' file | sed 1d
1
  • you can write awk -v RS='' ' /^pattern/ { sub(/[^\n]*\n/, ""); buf=$0 } END { if (buf!="") print buf }' infile, so no need add an extra sed deletion. Commented Jun 1, 2021 at 8:05

You must log in to answer this question.

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