powershellxmlnodelist

xmlNodeList not limited to selected nodes, and not providing correct parent node


I have source XML which has two child nodes of the root node. I pass each of those child nodes to a function as an [xml.xmlNodeList]. In that function, when I look at the count of the child nodes it is correct, 1 in the first call, passing the initialization node, and 2 in the second call using the processing node. However, if I then try to select the Grandchild nodes of a child nodes, the count goes all wrong. There are only 5 replacement nodes in the windows node under initialization, and 6 under processing, but the results I see are consistently 11 & 22. And yet the actual XML is correct, as shown with the write to console of $nodesToAdd. Where am I going so horribly wrong? I don't see anything that could be polluting the pipeline, which is my usual stumbling block.

$sourceXML = [xml] @"
<tokens>
  <initialization>
    <windows>
      <replacement id="psVersion" type="psVersiontable">psVersion</replacement>
      <replacement id="osID" type="regProperty" os="10.0">HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ReleaseId</replacement>
      <replacement id="osName" type="regProperty">HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProductName</replacement>
      <replacement id="osBuild" type="regProperty">HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentBuildNumber</replacement>
      <replacement id="osVersion" type="regProperty">HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentVersion</replacement>
    </windows>
  </initialization>
  <processing>
    <exitCode>
      <replacement id="successfulExecute" type="string">0, 3010</replacement>
      <replacement id="successfulInstall" type="string">0, 1641, 3010, -2147021886</replacement>
      <replacement id="successfulUninstall" type="string">0, 1641, 3010</replacement>
      <replacement id="wait" type="string">1618, -2147023278</replacement>
    </exitCode>
    <windows>
      <replacement id="commonAppData" type="specialFolder">CommonApplicationData</replacement>
      <replacement id="commonDesktop" type="specialFolder">CommonDesktopDirectory</replacement>
      <replacement id="commonDocuments" type="specialFolder">CommonDocuments</replacement>
      <replacement id="commonProgramFiles" type="specialFolder">CommonProgramFiles</replacement>
      <replacement id="commonProgramFilesX86" type="specialFolder">CommonProgramFilesX86</replacement>
      <replacement id="commonStartMenu" type="specialFolder">CommonStartMenu</replacement>
    </windows>
  </processing>
</tokens>
"@

function Set-PxTokenXml {
    param (
        [xml.xmlNodeList]$nodesToAdd
    )
    Write-PxXmlToConsole $nodesToAdd
    Write-Host "$($nodesToAdd.count)"

    $testNodes = $nodesToAdd.SelectNodes("//windows/*")
    Write-Host "$($testNodes.count)"
}

function Write-PxXmlToConsole ($xml) {
    $stringWriter = New-Object System.IO.StringWriter
    $xmlWriter = New-Object System.Xml.XmlTextWriter $stringWriter
    $xmlWriter.Formatting = "indented"
    $xml.WriteTo($xmlWriter)
    $xmlWriter.Flush()
    $stringWriter.Flush()
    Write-Host $stringWriter.ToString()
    Write-Host
    Write-Host
}

### MAIN
Clear-Host

Set-PxTokenXml ($sourceXML.SelectNodes('//initialization/*'))
Set-PxTokenXml ($sourceXML.SelectNodes('//processing/*'))

It seems that while $nodesToAdd is supposed to be a child node, it is actually the entire XML, so "//windows/*" is getting all the replacement nodes that are children of windows, in both initialization and processing. So I tried this, getting the parent node of the passed nodes, and using that to refine the selection.

function Set-PxTokenXml {
    param (
        [xml.xmlNodeList]$nodesToAdd
    )
    #Write-PxXmlToConsole $nodesToAdd
    Write-Host "$($nodesToAdd.count)"
    $parentNode = $nodesToAdd.parentNode.name
    Write-Host "$parentNode"

    $testNodes = $nodesToAdd.SelectNodes("//$parentNode/windows/*")
    Write-Host "$($testNodes.count)"
}

But that errors, with the parent node name doubled.

Exception calling "SelectNodes" with "1" argument(s): "'//processing processing/windows/*' has an invalid token."

That doubling is related to the number of child nodes. If I add a third child node under processing the I get 'processing processing processing' as the name of the parent node.

The idea was to only pass the nodes I actually want to work with, and keep the number of arguments down. If I pass the entire XML and the name of the node I want to draw from (initialization or processing) I can get it to work. Just curious why xmlNodeList behaves this way, and if there is somehow a way to get a single parent node and make this work with fewer arguments.

EDIT: Per Ansgar I now have this

function Set-PxTokenXml {
    param (
        [xml.xmlNodeList]$nodesToAdd
    )
    Write-PxXmlToConsole $nodesToAdd
    Write-Host "$($nodesToAdd.count)"

    $testNodes = $nodesToAdd.SelectNodes("./windows/*")
    Write-Host "$($testNodes.count)"
}

and now $testNodes.count is coming back 0. For both calls. PS5 if that makes a difference, though I hope not as I need to support PS2, at least in this early part of the code.


Solution

  • XML objects are a little weird. You are passing the selected child nodes to Set-PxTokenXml, however, those nodes still have access to the entire XML structure (otherwise you wouldn't be able to e.g. access their parent node). Because of that an XPath expression starting with // will look anywhere below the XML root node, not just below the nodes you were passing into the function. The correct XPath expression for expressing "below the current node" is ./.

    Also, you probably want to pass the parent nodes (<initialization> and <processing>) into Set-PxTokenXml, not the children of those nodes.

    Change the line

    $testNodes = $nodesToAdd.SelectNodes("//windows/*")
    

    into

    $testNodes = $nodesToAdd.SelectNodes("./windows/*")
    

    and change these lines

    Set-PxTokenXml ($sourceXML.SelectNodes('//initialization/*'))
    Set-PxTokenXml ($sourceXML.SelectNodes('//processing/*'))
    

    into

    Set-PxTokenXml ($sourceXML.SelectNodes('//initialization'))
    Set-PxTokenXml ($sourceXML.SelectNodes('//processing'))
    

    and the code will do what you expect.