powershellxml-parsing

Powershell XML count child nodes


Hello,

I have this XMLFile and I try to get how many SaveDrive, SaveFolder and Save File elements there are.


<?xml version="1.0"?>
<CreationFile transactionDate="18-AUG-2024 09:52:18" userName="Diana" Title="A-12345">
  <function name="SaveDrive" FuncType="Sync">
    <Title>A-12345</Title>
    <Facility>MMLD</Facility>
    <Area>COLOMBIA</Area>
    <Version>V1</Version>
  </function>
  <function name="SaveFolder" FuncType="Sync">
    <Folder>H1</Folder>
    <Title>A-12345</Title>
    <FolderType>Docs</FolderType>
  </function>
  <function name="SaveFile" FuncType="Sync">
    <Title>A-12345</Title>
    <Folder>H1</Folder>
    <File>Text1</File>
  </function>
  <function name="SaveFile" FuncType="Sync">
    <Title>A-12345</Title>
    <Folder>H1</Folder>
    <File>Text2</File>
  </function>
</CreationFile>

And I try to get the number of items based on the function, for this example I want to have:
SaveDrive: 1
SaveFolder: 1
SaveFile: 2

Then first I load the xml file

[xml]$ConfigXML = Get-Content -Path $xmlFileNamePath
$dummy = $ConfigXML.OuterXml
$firstline = '<?xml version="' + '1.0"?>'
$dummy = $dummy.Replace($firstline, "")
$ConfigXML = New-Object -TypeName System.Xml.XmlDocument
$dummy = $WTUConfigXML.LoadXml($dummy)

and try to get the elements using someone code already on this page.

[scriptblock]$DriveFilter  = {$_.name -eq "SaveDrive"}
[scriptblock]$FolderFilter = {$_.name -eq "SaveFolder"}
[scriptblock]$FileFilter   = {$_.name -eq "SaveFile"}

$found_elements = $ConfigXML.selectnodes("//CreationFile/function") | Where-Object ($DriveFilter)
$total += $found_elements.Count
Write-Host $found_elements.count.ToString("n0") elements

$found_elements = $ConfigXML.selectnodes("//CreationFile/function") | Where-Object ($FolderFilter)
$total += $found_elements.Count
Write-Host $found_elements.count.ToString("n0") elements

$found_elements = $ConfigXML.selectnodes("//CreationFile/function") | Where-Object ($FileFilter)
$total += $found_elements.Count
Write-Host $found_elements.count.ToString("n0") elements

**Then what it's my issue: **
for DriveFilter is working as expected and return 2 but for $DriveFilter and $FolderFilter, those only have 1 element, it gives me this error. If I add more SvaeDrive and SaveFolder it works, I mean after 2 element is working but for 1 is not.

**You cannot call a method on a null-valued expression.
At E:\CygNet\Corporate\PDO\Scripts\WellTest\NibrasWTIntegration.ps1:90 char:2
Write-Host $found_elements.count.ToString("n0") elements

  • ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
    

CategoryInfo : InvalidOperation: (:) [], RuntimeException
FullyQualifiedErrorId : InvokeMethodOnNull**

How to solve this? I would like not to use a foreach loop as below, it works but I dont like

$totalDrive  = 0;
$totalFolder = 0;
$totalFile   = 0;

foreach ($item in ($ConfigXML.SelectNodes("//CreationFile/function") | Where-Object $DriveFilter)) {
    $totalDrive += 1;
}

foreach ($item in ($ConfigXML.SelectNodes("//CreationFile/function") | Where-Object $FolderFilter)) {
    $totalFolder += 1;
}

foreach ($item in ($ConfigXML.SelectNodes("//CreationFile/function") | Where-Object $FileFilter)) {
    $totalFile += 1;
}

Write-Host WTU $totalWTU with $totalHeader and $totalPort ports

Thanks,


Solution

  • tl;dr

    mclayton has provided the crucial pointer in a comment:

    You're seeing a bug (see below) in Windows PowerShell that has since been fixed in PowerShell (Core) 7.

    The workaround is to enclose your pipelines in @(...), the array-subexpression operator, to ensure that their output becomes an array whose .Count property you can access predictably; e.g.:

    $found_elements =
      @($ConfigXML.SelectNodes("//CreationFile/function") | Where-Object $DriveFilter)
    

    Background information:

    Since v3, PowerShell has unified handling of scalars and array-like collections, via providing intrinsic properties that allow you to treat a scalar (single object) like an array, namely through exposing a .Count property (and a .Length alias) reporting 1 (try (Get-Date).Count, for instance), and through allowing indexing, with [0] and [-1] reporting the object itself (e.g., (Get-Date)[0] is the same as Get-Date) - unless the scalar's type implements such members itself.

    The bug at hand is that in Windows PowerShell [System.Xml.XmlNode] instances unexpectedly do not have intrinsic .Count (and .Length) properties, even though they should.

    The reason that matters is that even though the SelectNodes() method always returns a(n array-like) list of nodes, of type System.Xml.XmlNodeList, which does have a .Count property, PowerShell's auto-enumeration behavior streams its elements one by one to the pipeline; if the Where-Object call filters in only one of those elements, capturing the pipeline output in a variable captures that element as-is; it is only two or more output objects that - of necessity - are captured in an array (see this answer for background information).

    Therefore, if your pipeline outputs just one node, your attempt to call .Count on the XmlNode instance quietly evaluates to $null due to the absence of the intrinsic .Count property.

    The subsequent attempt to call a method on this $null value ($found_elements.count.ToString("n0")) therefore causes the error you saw.

    As noted, ensuring that the pipeline output is always collected in an array, even if there's only one (or no) output object, using @(...), works around this problem.