xmlxmlstarlet

Return values based on dynamic expression in XmlStarlet


I want to get the text element based on the values given in "part1".

In below example XML, I want Text2 and Text5 to be returned, because the values in part1 refer to 2 and 5.

How can this be done using XmlStarlet, preferably under Windows.

I started with:

xml sel -t -m //part1/line -v @val -o "," -c ../../part2/line[@val='3']/text -n example.xml

giving:

2,<text>Text 3</text>
5,<text>Text 3</text>

Which is, of course not correct because I do want want the fixed 3, but that should be dynamic, like:

xml sel -t -m //part1/line -v @val -o "," -c concat('../../part2/line[@val=',@val,']/text') -n example.xml

which returns, the Xml-path, and not the value which I was expecting:

2,../../part2/line[@val=2]/text
5,../../part2/line[@val=5]/text

I am expecting the next text to be returned.

2,<text>Text 2</text>
5,<text>Text 5</text>

(The <text> and </text> are not really needed here...)

Can the be done using XmlStarlet (preferably under Windows)?

The example.xml is:

<root>
  <part1>
      <line val="2"></line>
      <line val="5"></line>
  </part1>
  <part2>
      <line val="1">
           <text>Text 1</text>
      </line>
      <line val="2">
           <text>Text 2</text>
      </line>
      <line val="3">
           <text>Text 3</text>
      </line>
      <line val="4">
           <text>Text 4</text>
      </line>
      <line val="5">
           <text>Text 5</text>
      </line>
      <line val="6">
           <text>Text 6</text>
      </line>
  </part2>
</root>

EDIT: As always, on gets an idea after posting the question

I would appreciate an easier solution than (which produces the correct results!):

for /f "usebackq tokens=*" %f in (`xml sel -t -m //part1/line -v @val -n example.xml`) do @xml sel -t -m //part2/line[@val=%f]/text -v "%f" -o "," -v . -n example.xml

Solution

  • The thing to remember when using sel in xmlstarlet is that it's used to create XSLT to do the query. This means we have current() available to us.

    Try:

    xml sel -t -m "//part1/line/@val" -v "concat(.,',',//part2/line[current()=@val]/text)" -n example.xml
    

    It produces:

    2,Text 2
    5,Text 5
    

    If we add -C to the command line, we can see the XSLT that was produced...

    <?xml version="1.0"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" version="1.0" extension-element-prefixes="exslt">
      <xsl:output omit-xml-declaration="yes" indent="no"/>
      <xsl:template match="/">
        <xsl:for-each select="//part1/line/@val">
          <xsl:call-template name="value-of-template">
            <xsl:with-param name="select" select="concat(.,',',//part2/line[current()=@val]/text)"/>
          </xsl:call-template>
          <xsl:value-of select="'&#10;'"/>
        </xsl:for-each>
      </xsl:template>
      <xsl:template name="value-of-template">
        <xsl:param name="select"/>
        <xsl:value-of select="$select"/>
        <xsl:for-each select="exslt:node-set($select)[position()&gt;1]">
          <xsl:value-of select="'&#10;'"/>
          <xsl:value-of select="."/>
        </xsl:for-each>
      </xsl:template>
    </xsl:stylesheet>
    

    The unfortunate thing is that there is no way to use xsl:key which would be the most efficient way to do this query. You could however write the XSLT using xsl:key and execute it with xmlstarlet. Although more efficient, it's a little more complex and you were looking for "easier".