'Add String to New Line After Final Pattern Match

I am trying to add the follow string after the last pattern match in a text file.

Text File

<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>

Attempted Code

sed -i '/<\/Polygon>/a</\MultiGeometry>' text_file

This code inserts </MultiGeometry> after each match of </Polygon> instead of the last match of </Polygon> in the text file.

Current Result

<MultiGeometry>
<Polygon>1
</Polygon>
</MultiGeometry>
<Polygon>2
</Polygon>
</MultiGeometry>
<Polygon>3
</Polygon>
</MultiGeometry>
<Polygon>4
</Polygon>
</MultiGeometry>
<Polygon>5
</Polygon>
</MultiGeometry>

Expected Result

<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>
</MultiGeometry>


Solution 1:[1]

A Perl solution:

tac text_file | perl -lpe 'next LINE if $seen; s{</Polygon>}{$&</MultiGeometry>} and $seen++;' > temp
mv temp text_file

tac : print lines in reverse order, from last to first.

The Perl one-liner uses these command line flags:
-e : Tells Perl to look for code in-line, instead of in a file.
-n : Loop over the input one line at a time, assigning it to $_ by default.
-p : Loop over the input one line at a time, assigning it to $_ by default. Add print $_ after each loop iteration.
-l : Strip the input line separator ("\n" on *NIX by default) before executing the code in-line, and append it when printing.

$& : matched pattern, here </Polygon>.
$seen : undef (false) if we have not replaced yet, and 1 (true) if we have. Enables doing 1 and only 1 replacement of the pattern, the first one from the end = the last one in the original file.

Solution 2:[2]

Using sed but indicating $ (last line in file or in LHS result) as an address

sed -irn '/<[/]Polygon>/ { $ s@</Polygon>@&</MultiGeometry>@ }' list1.txt

sed expression parts

/<[/]Polygon>/ get all lines matching pattern
{ $ s@</Polygon>@&</MultiGeometry>@ } make the replacement only on last line of matched ones. $ means last line, & refers to the whole LHS pattern.

Note: @ is used instead of / in s/ / / sed expression for better readability since forward slashes do not require escaping.

Solution 3:[3]

Using gnu-sed you can use -z flag and match .*</Polygon> to make sure to match last </Polygon> because .* is greedy to match longest match before matching </Polygon>.

sed -z 's~.*</Polygon>~&\n</MultiGeometry>~' file

<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>
</MultiGeometry>

Solution 4:[4]

Using sed

$ sed -e '\|</Polygon>|{$a\</MultiGeometry>' -e '}' input_file
<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>
</MultiGeometry>

Solution 5:[5]

Here is another way of doing this, with tac + awk + tac combination.

tac Input_file |
awk -v RS="" '{sub(/<\/Polygon>/,"</MultiGeometry>\n&")} 1' | 
tac

Explanation: Simple explanation would be, using tac on Input_file first to print lines of file into reverse order(from bottom to first line) and passing its output as an input to awk program. In awk program setting RS(record separator) as NULL and in its main program using sub to substitute </Polygon> with </MultiGeometry> and itself(using &), then using 1 is an awksh way to print current line. Further sending awk code's output as an input to tac command to get actual order of lines.

Solution 6:[6]

Here's a relatively flexible solution that can account for more than just polygons :

CODE

{m,g}awk -v __='Octinions' '

BEGIN {
    OFS = FS = (__) ">"
          sub(".$","[&]",FS)
     RS = "^$"
    ORS = ""
} END {
    if (NF == !_) {
        print "none found"
        exit
    }
    if (($!_) ~ "[^<]$") {
        match($!_, "[^<>\n]*$")
        ___ = "</" substr($!_, RSTART) (OFS)

    } else {
        match($!_, "[^\n<>]+[>][\n][<]?[/]?$")
        ___ = "</" substr($!_, RSTART)

        sub("[\n][^\n]*$","",___)
    }
    sub("[\n]","&"___"&",$NF)
    print
}'

OUTPUT

<Category_Fields>
<Fields>1
</Fields>
<Fields>2
</Fields>
<Fields>3
</Fields>
<Fields>4
</Fields>
<Fields>5
</Fields>

<Category_Transcendatals>
<Transcendatals>1
</Transcendatals>
<Transcendatals>2
</Transcendatals>
<Transcendatals>3
</Transcendatals>
<Transcendatals>4
</Transcendatals>
<Transcendatals>5
</Transcendatals>

<Category_Distributions>
<Distributions>1
</Distributions>
<Distributions>2
</Distributions>
<Distributions>3
</Distributions>
<Distributions>4
</Distributions>
<Distributions>5
</Distributions>

<Category_Octinions>
<Octinions>1
</Octinions>
<Octinions>2
</Octinions>
<Octinions>3
</Octinions>
<Octinions>4
</Octinions>
<Octinions>5
</Octinions>

</Category_Octinions>

<Category_EuclidianPlane>
<EuclidianPlane>1
</EuclidianPlane>
<EuclidianPlane>2
</EuclidianPlane>
<EuclidianPlane>3
</EuclidianPlane>
<EuclidianPlane>4
</EuclidianPlane>
<EuclidianPlane>5
</EuclidianPlane>

<Category_ComplexPlane>
<ComplexPlane>1
</ComplexPlane>
<ComplexPlane>2
</ComplexPlane>
<ComplexPlane>3
</ComplexPlane>
<ComplexPlane>4
</ComplexPlane>
<ComplexPlane>5
</ComplexPlane>

<Category_PartialDiff>
<PartialDiff>1
</PartialDiff>
<PartialDiff>2
</PartialDiff>
<PartialDiff>3
</PartialDiff>
<PartialDiff>4
</PartialDiff>
<PartialDiff>5
</PartialDiff>

<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>

Solution 7:[7]

This might work for you (GNU sed):

 sed 'H;1h;$!d;x;s/.*<\/Polygon>/&\n<\/MultiGeometry>/' file

Slurp the file into the hold space and using greed, append the required string after the last occurrence of </Polygon>.

Alternative, more memory efficient (probably slower):

sed '/<\/Polygon>/bb
     x;/./!{x;p;d};x;H
     :a;$!d;x;s/.*<\/Polygon>/&\n<\/MultiGeometry>/p;d
     :b;x;/./p;x;h;ba' file

Before the first occurrence of </Polygon>, print lines as normal.

Thereafter, keep a copy of lines following </Polygon> and only print their contents when the current line is </Polygon>.

At the end of the file, search the hold space for the line containing </Polygon> and append the required string.

Solution 8:[8]

With vim ex mode

# to stdout
$ printf '%s\n' '0?</Polygon>?a' '</MultiGeometry>' . %p | ex input_file
<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>
</MultiGeometry>

# inplace
$ printf '%s\n' '0?</Polygon>?a' '</MultiGeometry>' . x | ex input_file

$ cat input_file
<MultiGeometry>
<Polygon>1
</Polygon>
<Polygon>2
</Polygon>
<Polygon>3
</Polygon>
<Polygon>4
</Polygon>
<Polygon>5
</Polygon>
</MultiGeometry>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Timur Shtatland
Solution 2 LMC
Solution 3 anubhava
Solution 4 HatLess
Solution 5 RavinderSingh13
Solution 6 RARE Kpop Manifesto
Solution 7
Solution 8