How to swap the position of siblings in xml using xmlstarlet

84 Views Asked by At

I have the following xml file that I would like to swap the positions of the two siblings ("firstname" and "surname"):

From:

<?xml version="1.0" encoding="utf-8"?>
<article>
    <name>
     <firstname>Peter</firstname>
     <surname>Frenchman</surname>
    </name>
    <name>
     <firstname>Betty</firstname>
     <surname>Jones</surname>
    </name>
</article>

to:

<?xml version="1.0" encoding="utf-8"?>
<article>
    <name>
     <surname>Frenchman</surname>
     <firstname>Peter</firstname>   
    </name>
    <name>
     <surname>Jones</surname>
     <firstname>Betty</firstname>
    </name>
</article>

This is what I did:

I created a temp element (surname1) and copy the values from "surname" to it. The xmlstarlet command did not do the work:

xmlstarlet ed  -L   -i "//article/name/givenname" -t elem -n "surname1" -v "$(xmlstarlet sel -t -v  "//article/name/surname" swapelment.xml)" swapelment.xml
2

There are 2 best solutions below

2
Yitzhak Khabinsky On

Here is how to implement it via XSLT.

It is using a so called Identity Transform pattern.

XMLStarlet natively supports XSLT transformation: XMLStarlet Overview

Usage: xml tr <xsl-file> <xml-file-or-uri>

Input XML

<?xml version="1.0" encoding="utf-8"?>
<article>
    <name>
        <firstname>Peter</firstname>
        <surname>Frenchman</surname>
    </name>
    <name>
        <firstname>Betty</firstname>
        <surname>Jones</surname>
    </name>
</article>

XSLT

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes" method="xml" encoding="utf-8"/>
    <xsl:strip-space elements="*"/>

    <!-- IdentityTransform -->
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="name">
        <xsl:copy>
            <xsl:copy-of select="surname"/>
            <xsl:copy-of select="firstname"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Output XML

<?xml version="1.0" encoding="utf-8"?>
<article>
  <name>
    <surname>Frenchman</surname>
    <firstname>Peter</firstname>
  </name>
  <name>
    <surname>Jones</surname>
    <firstname>Betty</firstname>
  </name>
</article>
0
urznow On

Using xmlstarlet edit to move an XML element to a specified position can be done like this:

  1. insert a new empty node as destination, -i for a preceding sibling, -a for a following sibling
  2. copy the contents of source to the new node
  3. delete source after copying

For an input file containing a series of name elements, each with a firstname and a surname child element, swopping all can be done like this:

# shellcheck  shell=sh  disable=SC2016
xmlstarlet edit \
  --var F '*/name/surname' \
  -i '$F/preceding-sibling::*[1]' -t 'elem' -n 'surname' \
  -u '$prev' -x 'following-sibling::surname[1]/node() | following-sibling::surname[1]/@*' \
  -d '$F' \
file.xml

where:

  • the multi-member source node-set is referenced by variable F
  • -i … inserts a surname before each firstname in $F (firstname being the first preceding sibling element of surname)
  • -x specifies a relative XPath expression to handle the multi-member node-set in $F, the context node being the newly created surname which should duplicate its first following sibling surname
  • -x '…/node() | …/@*' makes a deep copy of source's nodes on the child and attribute axes (using a union) -- since surname doesn't actually have any attributes | …/@* can be omitted
  • -d '$F' deletes the original surnames, after copying

--var defines a named variable, and the back reference prev (aka xstar:prev) variable refers to the node(s) created by the most recent -s, -i, or -a option which all define or redefine it (see xmlstarlet.txt for examples of --var and $prev).