There is a classic rule of thumb in programming that says there are only three useful numbers - zero, one and many. I’ve found this concept very useful when writing tests as code that deals with collections or item counts sometimes need to handle these 3 cases in different ways. As a simple example imagine generating a log message about how many items you’re going to process. The lazy approach would be to just print the number and append a “(s)” to the noun to make it appear as though you’ve made an effort:-
Found 2 file(s) to process…
If you wanted to spell it out properly you’d write 3 separate messages:-
- No files need processing
- Found 1 file to process…
- Found 2 files to process…
A PowerShell Gotcha
This idea of 0, 1 & many is also the way I remember how PowerShell collections work when they are returned from a cmdlet. I was reminded of this idiom once again after debugging a colleague’s script that was failing because they had written this:-
$items = Get-SomeItems . . .
if ($items.Count -gt 0) {
. . .
For those not well versed in PowerShell this kind of construct will generate an error when no item or just 1 item is returned. The error will tell you “Count” is not a property on the variable - something like this in fact:-
Property 'Count' cannot be found on this object. Make sure that it exists.
At line:1 char:55
+ . . .
+ CategoryInfo : InvalidOperation: . . .
+ FullyQualifiedErrorId : PropertyNotFoundStrict
You won’t see this error unless you have Strict Mode turned on (hence the PropertyNotFoundStrict in the error message). For one-liners this might be acceptable, but when I’m writing a production grade PowerShell script I always start it with these two lines (plus a few others that I covered in “PowerShell, Throwing Exceptions & Exit Codes”):-
Set-StrictMode -Version Latest
$ErrorActionPreference="stop"
For those used to the family of Visual Basic languages the former is akin to the “Option Explicit” statement you probably learned to add after misspelling variables names a few times and then scratched your head as you tried to work out what on earth was going on.
PowerShell Collections
To help illustrate these three manifestations of a collection you might come across we can create 3 folders - an empty one, one with a single file and one with many files [1]:-
PS C:\temp> mkdir Empty | Out-Null
PS C:\temp> mkdir Single | Out-Null
PS C:\temp> echo single > .\Single\one-file.txt
PS C:\temp> mkdir Many | Out-Null
PS C:\temp> echo many > .\Many\1st-file.txt
PS C:\temp> echo many > .\Many\2nd-file.txt
Now, using Get-ChildItem we can explore what happens by invoking the GetType() method in the resulting value from the cmdlet to see exactly what we’re getting [2]:-
PS> $items = Get-ChildItem Empty; $items.GetType()
You cannot call a method on a null-valued expression.
PS> $items = Get-ChildItem Single; $items.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True FileInfo System.IO.FileSystemInfo
PS> $items = Get-ChildItem Many; $items.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True Object[] System.Array
As you can see in the first case we get a null reference, or in PowerShell terms, a $null. In the second case we get a single item of the expected type, and in the third an array of objects. Only the final type, the array, will have a property called “Count” on it. Curiously enough, as you might have deduced from earlier, you don’t get a warning about a “null-valued expression” if you try and access the missing Count property on a $null value, you get the “invalid property” error instead:-
PS C:\temp> $null.Count
Property 'Count' cannot be found on this object. Make sure that it exists.
Forcing a ‘Many’ Result
The idiomatic way to deal with this in PowerShell is not to try and do it in the first place. It is expected that you will just create a pipeline and pass the objects along from one stage to the next letting the PowerShell machinery hide this idiosyncrasy for you:-
PS C:\temp> Get-ChildItem Empty | Measure-Object |
Select Count
Count
-----
0
However, if you do need to store the result in a variable and then act on it directly [3] you’ll want to ensure that the variable definitely contains a collection. And to do that you wrap the expression in “@(...)”, like so:-
PS> $items = @(Get-ChildItem Empty);
Write-Output $items.Count
0
PS> $items = @(Get-ChildItem Single);
Write-Output $items.Count
1
PS> $items = @(Get-ChildItem Many);
Write-Output $items.Count
2
[1] Apologies for the old-skool syntax; I still work with a lot with batch files and the PowerShell syntax for creating directories just hasn’t bedded in yet. The blatant use of ECHO instead of Write-Output was me just being perversely consistent.
[2] Whilst Get-Member is the usual tool for inspecting the details of objects coming through a pipeline it will hide the different between a single value and a collection of values.
[3] For example diagnostic logging, which I tackled in “Logging & Redirection With PowerShell”.
No comments:
Post a Comment