Search code examples
xmlxpathxmlstarlet

XMLStarlet: Using xpath to move an element to be before another one


Given:

<x>
  <a />
  <d />
  <b />
  <c />
  <e />
  <f />
</x>

I would like to use xmlstarlet to move <d /> to be before <e />.

The closest I've got is:

echo .. | xml ed -m "//d" "//e"

Which produces:

<e>
  <d/>
</a>

This is unfortunately the example the manual gives.

echo .. | xml ed -m "//d" "//x"

Puts <d /> at the end, which is not the right place.

I tried to get preceding-sibling to work (if indeed that is right approach), but while:

echo .. | xml sel -t -c "//e/preceding-sibling::*[1]"

Results in <c />, that query doesn't work as a move destination (it complains that move destination is not a single node), nor would it really, since best case would be it would end up inside <c />.

I'm not sure if ed -m is the wrong approach, of if there is a form of XPATH which points to a location between elements instead of an element.

Edit: interestingly insert works more like how I'd expect, inserting what you pass it before the element picked with xpath:

$ xml ed -i "//c" -t elem -n "foo" -v "bar" test.xml
<?xml version="1.0"?>
<x>
  <a/>
  <d/>
  <b/>
  <foo>bar</foo>
  <c/>
  <e/>
  <f/>
</x>

Unfortunately the value passed (bar above) cannot be XML, so I could pick it out of somewhere with sel and then inject it in with this command I don't think.


Solution

  • This seems like it should be easy using ed. If it is, I'm not seeing it. (I don't use xmlstarlet very often.)

    You may need to use XSLT...

    XSLT 1.0

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output indent="yes"/>
      <xsl:strip-space elements="*"/>
    
      <xsl:template match="@*|node()">
        <xsl:copy>
          <xsl:apply-templates select="@*|node()[not(self::d)]"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="c">
        <xsl:copy-of select="."/>
        <xsl:copy-of select="../d"/>
      </xsl:template>
    
    </xsl:stylesheet>
    

    Command Line

    $ xml tr test.xsl test.xml
    <?xml version="1.0"?>
    <x>
      <a/>
      <b/>
      <c/>
      <d/>
      <e/>
      <f/>
    </x>