Description
In the following examples, we want to enclose every match of [0-9]+ with a pair of brackets except the last 5 ones, 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] h
[ 9] s/\n.*//
[10] s/[0-9]+/[&]/gp
[11] g
[12] D
[13] :final
[14] s/([0-9]+([^0-9]+|$)){1,5}$/\n&/
[15] h
[16] s/\n.*$//
[17] s/[0-9]+/[&]/g
[18] G
[19] s/\n[^\n]*\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
    • replace every match in it,
    • print it, and then examine the following lines.
    . These will be done via the following steps:
    • Step [8]: command `H' saves the entire PS to HS.
    • Step [9] removes from PS everything except the first line.
    • Step [10] replaces every match (in the first line of PS) and then prints it.
    • Step [11]: command `g' takes back from HS the data saved by Step [8]. Note that the first line is no longer used.
    • Step [12]: command `D' deletes the first line of PS, and then starts a new cycle. Since PS still contains at least N matches, Step [1] will NOT be performed.
  5. When the end of the file is reached, we are sure that PS contains all the last N, and others, if exist. The other matches, if exist, will be in the first line of PS. We still have to replace non-last-N matches:
    • Step [14] insert a newline character as a mark before the first of the last N matches.
    • Step [15]: command `h' saves the entire PS to HS.
    • Step [16] removes everything starting from the mark.
    • Step [17] replaces every match before the mark.