Search code examples
xmlreplacexmlstarlet

xmlstarlet foreach node update value


I'm trying to update all nodes with the same pattern using xmlstarlet. Given the following xml

<root>
  <application>
    <provider name="alpha" value="my.corp.lion" />
    <provider name="beta" value="my.corp.tiger" />
    <provider name="gamma" value="my.corp.monkey" />
  </application>
</root>

I can currently update each node as follows

oldCorp="my.corp"
newCorp="new.my.corp"
myNode="/root/application/provider[@name='alpha']/@value"
oldValue=$(xml sel -t -v ${myNode} MyXml.xml)
newValue=${oldValue//$oldCorp/$newCorp}
xml ed --inplace -u ${myNode} -v "${newValue}" MyXml.xml

# results in provider.alpha being new.my.corp.lion

What I'd like to be able to do however is foreach ALL provider nodes and update the final xml result to be

<root>
  <application>
    <provider name="alpha" value="new.my.corp.lion" />
    <provider name="beta" value="new.my.corp.tiger" />
    <provider name="gamma" value="new.my.corp.monkey" />
  </application>
</root>

Is there a way to do a foreach over the providers and replace all my.corp instances with new.my.corp?


Solution

  • In general, use the -x option of xmlstarlet like this:

    xmlstarlet ed --inplace -u "/root/application/provider/@value" -x "concat('new.my.corp',substring-after(.,'my.corp'))" input.xml
    

    In your special case, change the code to

    oldCorp="my.corp"
    newCorp="new.my.corp"
    myNode="/root/application/provider/@value"
    xml ed --inplace -u "${myNode}" -x "concat('${newCorp}',substring-after(.,'${oldCorp}'))" MyXml.xml
    

    The output is as desired:

    <?xml version="1.0"?>
    <root>
      <application>
        <provider name="alpha" value="new.my.corp.lion"/>
        <provider name="beta" value="new.my.corp.tiger"/>
        <provider name="gamma" value="new.my.corp.monkey"/>
      </application>
    </root>
    

    This code replaces all attribute values specified by the myNode XPath with the value constructed in the -x argument of xmlstarlet.

    AFAIK xmlstarlet does not support RegEx'es, so you have to create the replacement expressions with XPath-1.0 functions like substring-after(...), substring(...) and so on.


    If you need RegEx'es for your replacement, you have to use XPath-2.0 functions which are part of XSLT-2.0. Then you can use an XSLT-2.0 stylesheet like the following which achieves the same thing, but with RegEx'es:

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="xml" encoding="UTF-8" indent="yes"/>
      <xsl:param name="oldCorp" select="'my\.corp'" />
      <xsl:param name="newCorp" select="'new.my.corp'" />
    
      <!-- Identity template -->
      <xsl:template match="node()|@*">
        <xsl:copy>
          <xsl:apply-templates select="node()|@*" />
        </xsl:copy>
      </xsl:template>  
    
      <xsl:template match="provider">
        <xsl:copy>
          <xsl:copy-of select="@*" />
          <xsl:attribute name="value">
            <xsl:value-of select="replace(@value,$oldCorp,$newCorp)" />
          </xsl:attribute>
        </xsl:copy>
      </xsl:template>
    
    </xsl:stylesheet>
    

    You can pass the parameters as strings to the XSLT-2.0 processor. Both parameters are initialized with your default values.