Description
In the following examples, we want to enclose each of the last 5 matches of [0-9]+ with a pair of brackets, i.e., N=5 for this case.
Raw Input Desired Output
number 8888
number 777
6666 last fifth 55555 last fourth 4444
no numbers in this line
Last third 3333  last second 2222
Last number 111
no numbers
number 8888
number 777
number 6666 last fifth [55555] last fourth [4444]
no numbers in this line
Last third [3333]  last second [2222]
Last number [111]
no numbers
Script and Comments
Script1
[ 1] /[0-9]/!b
[ 2] :loop
[ 3] /\n.*([0-9]+([^0-9]+|$)){5}$/!{
[ 4] $b final
[ 5] N
[ 6] b loop
[ 7] }
[ 8] P
[ 9] D
[10] :final
[11] s/([0-9]+([^0-9]+|$)){1,5}$/\n&/
[12] h
[13] s/^[^\n]*\n//
[14] s/[0-9]+/[&]/g
[15] x
[16] s/\n.*$//
[17] G
[18] s/\n//
Comments -r
  1. There may be some other matches other than the last N ones in the same line as the last N-th match. For example, in the sample data, 6666 has to be replaced while 55555 and 4444 do not.
  2. To figure out whether a line contains any of the last N matches, the script appends the next lines to PS until the part starting from the second line till the end contains N matches.
  3. Steps [2] thru [7] constitute a loop which iterates till
    • the end of file reaches, or
    • the part starting from the second line of PS till the end contains N matches. In other words, the first line does NOT contain the last N matches.
    • GNU sed 4.2.1 does not interpret {5} in RE of Step [3] correctly, you have to expand it by yourself via writing [0-9]+([^0-9]+|$) 5 times and concatenate them together, like
      [0-9]+([^0-9]+|$)[0-9]+([^0-9]+|$)[0-9]+([^0-9]+|$)[0-9]+([^0-9]+|$)[0-9]+([^0-9]+|$).
  4. When the first line of PS does not contains the last N matches, we have to
    • print it, deletes it from PS,
    • and then examine the following lines.
    These will be done via the `P-D' sequence of Steps [8] and [9].
  5. When the end of the file is reached, we are sure that PS contains all the last N matches, and others, if exist. The other matches, if exist, will be in the first line of PS. What we have to do is to replace the last-N matches and leave others alone:
    • First divide PS into two parts with the second one containing all the last N matches but no others. This is done via Step [11] which inserts a newline character before the first of the last N matches.
    • Step [12]: command `h' saves the contents of PS to HS.
    • Replace the last N matches by:
      • first deletes the first part (Step [13]),
      • then perform the replacements (Step [14]).
      After this, PS contains only the modified second part.
    • Finally, take the first part back:
      • Step [15] exchanges the contents of PS and HS. After this, PS contains the first part, a newline character, and the original second part; HS contains the modified second part.
      • Step [16] removes the original second part from PS.
      • Step [17] appends the contents of HS to PS. After this, PS contains the first part, a newline character, followed by the modified second part.
      • Step [18] removes the embedded newline generated by `G' of Step [17].