Search code examples
xmlxpathxmlstarlet

What is the XML/XPath expression to use with xmlstarlet to move an element to be the first element?


I have an XML file similar to:

<?xml version="1.0"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <Folder>
      <name>Assets and Risks</name>
      <Placemark>
        <name>Asset_4000</name>
        <description>Address: 2286</description>
        <Point>
          <coordinates>xxx.xxx,yyy.yyy,0</coordinates>
        </Point>
      </Placemark>
      <Placemark>
        <name>Risk_2000</name>
        <description>Address: 32</description>
        <Point>
          <coordinates>xxx.xxx,yyy.yyy,0</coordinates>
        </Point>
      </Placemark>
    </Folder>
    <Folder>
      <name>The second folder</name>
    </Folder>
  </Document>
</kml>

I want to use xmlstarlet (preferably all command line rather than XSLT) to move the Placemark for Risk_2000 to be the first Placemark within the same Folder (ie. before the Placemark for Asset_4000).

I know the first part is:

xmlstarlet edit --move "//_:kml/_:Document/_:Folder[_:name=\"Assets and Risks\"]/_:Placemark[_:name=\"Risk_2000\"]" **But What Goes Here**

Any guidance appreciated.


Solution

  • The destination for xmlstarlet edit's --move must be a single node, so work around:

    • insert temporary element at position N
    • update it from source element
    • delete source element
    • rename temporary element

    (For a non-GNU/Linux platform adjust quoting and line continuation characters in following command.)

    xmlstarlet edit -N v='http://www.opengis.net/kml/2.2' \
        --insert '//v:Folder[v:name="Assets and Risks"]/v:Placemark[1]' \
            --type elem --name 'Placemark_TMP' --value '' \
        --update '$xstar:prev' --expr '../v:Placemark[v:name="Risk_2000"]/node()' \
        --delete '$xstar:prev/../v:Placemark[v:name="Risk_2000"]' \
        --rename '$xstar:prev' --value 'Placemark' \
      file.xml | xmlstarlet format --nsclean
    

    xmlstarlet edit code can use the convenience $xstar:prev (aka $prev) node to refer to the node created by the most recent -i / --insert, -a / --append, or -s / --subnode option. Examples of $xstar:prev are given in doc/xmlstarlet.txt and the source code's examples/ed-backref*.

    --expr '../v:Placemark[…]/node()' makes a deep copy of element's child nodes but not its attributes (background). The element in question has no attributes, otherwise use the XPath --expr '../v:Placemark[…]/node() | ../v:Placemark[…]/@*'.

    The --nsclean step removes redundant namespace declarations.

    Since xmlstarlet supports there may be possibilities in the set:leading or set:trailing functions but I didn't look into it.

    For a stylesheet-based approach see this.


    UPDATE 2021-09-29

    The code above can be rewritten as follows using

    • short commands and options
    • the --var <name> <xpath> option (in doc/xmlstarlet.txt but not in the user's guide)
    • the default namespace shortcut _:

    and adding | $src/@* to include attributes:

    xmlstarlet ed \
        --var dir '//_:Folder[_:name="Assets and Risks"]' \
        --var tgt '$dir/_:Placemark[1]' \
        --var src '$dir/_:Placemark[_:name="Risk_2000"]' \
        -i '$tgt' -t elem -n 'Placemark_TMP' -v '' \
        -u '$xstar:prev' -x '$src/node() | $src/@*' \
        -d '$src' \
        -r '$xstar:prev' -v 'Placemark' \
        file.xml | xmlstarlet fo -N