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