Search code examples
xmlpowershellcsvxpathapostrophe

Passing xpath from CSV to replace value in XML with Powershell


Looking to read a list of xml elements (as xpaths) & their new values from a csv file -> and then, look into an existing xml file to replace whatever xml elements are to be updated, with the new values on the given xpaths.

I got the values for the xpaths I need to change in the big xml by online parsing it. There can/will be any number of values to replace at one time, so I can't hard-code these into the powershell - it will need to read each set of changes required each time - from a file (the csv), I am suggesting.

One xpath target to replace a value is is :

    /configuration/services/objects/object/app-args/list/value[0]/text()

So I have the csv line as:

    "/configuration/services/objects/object/app-args/list/value[0]/text()","new value"

I then call a fn to replace this xpath with the new value, in the xml.

I am struggling on just that one line, where the "-" in app-args xpath string above, is being parsed and somehow cutting the xpath string short there, and therefore tripping out the replace. I can't change the node names in the xml - not an option.

I can browse the xml structure in powershell when I load it:

    $xml = [xml](Get-Content "C:\temp\big.xml")

So autocomplete browsing on prompt, I can then see that:

    PS c:\temp> $xml.configuration.services.objects.object.'app-args'.list.value[0]

will show me the correct "replace this value 1" in the xml.

But I can't get the xpath phrasing correct in the csv to replace this node. How can I read that node with the "-" in the name, from a file? I've tried /"app-args"/ and a host of other escape chars but out of ideas for now. Use something other than csv?

No experience with xpath before and it's really killing me!

Ref: (simplified) xml with problem area is:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <spring xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.net ../../../../lib/spring.net/spring-objects-1.1.xsd">
    <context>
      <resource uri="config://spring/objects" />
      <context name="services">
        <resource uri="config://spring/services/objects" />
      </context>
    </context>
    <objects xmlns="http://www.springframework.net" default-autowire="constructor">
      <object name="SomeServicefactory" type="ServiceFactory" />
    </objects>
    <services>
      <objects xmlns="http://www.springframework.net" default-lazy-init="true">
        <object name="SystemConfiguration" type="SystemConfiguration">
          <app-args name="ConfigCodes">
            <list element-type="string">
              <value>old value</value>
              <!-- Automate me -->
              <!-- use case, assume default value as per above, allow the powershell module to override this -->
              <value>old value</value>
              <!-- Automate me -->
              <!-- use case, assume default value as per above, allow the powershell module to override this -->
            </list>
          </app-args>
        </object>
      </objects>
    </services>
  </spring>
</configuration>

And the most simple csv I am using is (xpath,new value for it):

    "/configuration/services/objects/object/app-args/list/value[0]/text()","new value 1"
    "/configuration/services/objects/object/app-args/list/value[1]/text()","new value 2"

Solution

  • Since you are providing an example which has namespace, that will have a little bit of a different way to define your paths.

    Your XML tags namespace at objects, so everything under objects is considered to be part of a specific namespace. This namespace is necessary when looking up, editing or deleting nodes.

    $xml.SelectSingleNode("/configuration/services/x:objects/x:object/x:app-args/x:list/x:value[1]", $Namespace)
    $Namespace = New-Object -TypeName "Xml.XmlNamespaceManager" -ArgumentList $xml.NameTable
    $Namespace.AddNamespace("x", "http://www.springframework.net")
    
    

    Since /configuration/services are outside of that namespace, you dont have to tag them with that namespaceManager. But everything under, and including, objects will have to be tagged.

    If you are interested in changing the values, you will need to update the InnerText of the elements.

    $xml.DocumentElement.SelectSingleNode("/configuration/services/x:objects/x:object/x:app-args/x:list/x:value[1]", $Namespace).InnerText = "New Value for First"
    
    # or you can use // to search for the first element
    $xml.DocumentElement.SelectSingleNode("//x:objects/x:object/x:app-args/x:list/x:value[1]", $Namespace).InnerText = "New Value for First"
    

    Also note, value is indexed starting with 1. There is no value[0]. From all of this, your csv will need to be updated to include the namespace tags.

    "//x:objects/x:object/x:app-args/x:list/x:value[1]","new value 1"
    "//x:objects/x:object/x:app-args/x:list/x:value[2]","new value 2"
    

    Complete Code

    $Namespace = New-Object -TypeName "Xml.XmlNamespaceManager" -ArgumentList $xml.NameTable
    $Namespace.AddNamespace("x", "http://www.springframework.net")
    
    $csv = Get-Content C:\temp\csv.txt
    [xml]$xml = Get-Content C:\temp\xml.txt
    
    foreach($line in $csv) {
        $items = $line.Replace("""", "").Split(",") # Remove extra quotes
        $xml.DocumentElement.SelectSingleNode($items[0], $Namespace).InnerText = $Items[1]
    }
    $xml.Save("C:\temp\new.txt")
    

    new.txt content:

    <configuration>
      <services>
        <objects xmlns="http://www.springframework.net" default-lazy-init="true">
          <object name="SystemConfiguration" type="SystemConfiguration">
            <app-args name="Codes">
              <list element-type="string">
                <value>new value 1</value>
                <value>new value 2</value>
              </list>
            </app-args>
          </object>
        </objects>
      </services>
    </configuration>
    

    --------------------------------------------------------

    Suggested Solution with updated question

    Based on your new xml, i would recommend going with the Select-XML route.

    CSV

    "//ns:objects/ns:object/ns:app-args/ns:list/ns:value[1]","new value 1"
    "//ns:objects/ns:object/ns:app-args/ns:list/ns:value[2]","new value 2"
    

    CODE

    $csv = Get-Content C:\temp\csv.txt
    [xml]$xml = Get-Content C:\temp\xml.txt
    
    $ns = @{ns='http://www.springframework.net'}
    foreach($line in $csv) 
    {
        $items = $line.Replace("""", "").Split(",") # Remove extra quotes
        $nodeRef = Select-Xml -Xml $xml -XPath $items[0] -Namespace $ns
        $nodeRef.Node.InnerText = $items[1]
    }
    
    $xml.Save("C:\temp\new.txt")
    

    New file has the values updated.

    Please mark the post answered if it answers the issue in your post.