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.
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.