I am trying to perform some simple if statements, but all of the newer cmdlets that are based upon [Microsoft.Management.Infrastructure.CimInstance] don't seem to expose a .count method?
$Disks = Get-Disk
$Disks.Count
Doesn't return anything. I found that I can cast this as an [array], which makes it returns a .NET .count method as expected.
[Array]$Disks = Get-Disk
$Disks.Count
This works without directly casting it as an array for previous cmdlets:
(Get-Services).Count
What is the recommended way to get around this?
An example that doesn't work:
$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
Else If ($PageDisk.Count -eq 1) { Do X }
Option A (Cast as Array):
[Array]$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
Else If ($PageDisk.Count -eq 1) { Do X }
Option B (Use Array Indexes):
$PageDisk = Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)}
If ($PageDisk[0] -eq $Null) {Write-Host "No suitable drives."; Continue}
Else If ($PageDisk[1] -ne $Null) {Write-Host "Too many drives found, manually select it."}
Else If (($PageDisk[0] -ne $Null) -and (PageDisk[1] -eq $Null)) { Do X }
Option C (Array) -Thanks to @PetSerAl :
$PageDisk = @(Get-Disk | Where {($_.IsBoot -eq $False) -and ($_.IsSystem -eq $False)})
If ($PageDisk.Count -lt 1) {Write-Host "No suitable drives."; Continue}
Else If ($PageDisk.Count -gt 1) {Write-Host "Too many drives found, manually select it."}
Else If ($PageDisk.Count -eq 1) { Do X }
What is the reason for CIM based cmdlets not exposing the .Count method? What is the recommended way to handle this? Option B seems convoluted to me, and hard to read. Option A works, but shouldn't powershell cast this as an array for me? Am I going about this in entirely the wrong way?
In PSv3+, with its unified handling of scalars and collections, any object - even $null
- should have a .Count
property (and, with the exception of $null
, should support indexing with [0]
).
Any occurrence of an object not supporting the above should be considered a bug.
For instance, [pscustomobject]
instances not playing by these rules is a known bug.
Since I don't know if said bug is related to the [Microsoft.Management.Infrastructure.CimInstance#ROOT/Microsoft/Windows/Storage/MSFT_Disk]
instances that Get-Disk
outputs, and since Get-Disk
- at least currently - is only available in Windows PowerShell, I encourage you to file a separate bug on uservoice.com.
Use of array-subexpression operator @(...)
is only necessary:
Generally, if you do need to ensure that something is an array, use @(...)
rather than [Array] ...
/ [object[]] ...
- @()
is PowerShell-idiomatic, more concise, and syntactically easier.
That said, given that @()
technically creates a (shallow) copy of an existing array, you may prefer [Array]
when dealing with potentially large arrays.
Additionally, @(...)
and [Array] ...
are not generally equivalent, as PetSerAl's helpful examples in a comment on the question demonstrate; to adapt one of his examples:
@($null)
returns a single-item array whose one and only element is $null
, whereas [Array] $null
has no effect (stays $null
).
This behavior of @()
is consistent with its purpose (see below): since $null
is not an array, @()
wraps it in one (resulting in a [System.Object[]]
instance with $null
as the only element).
In PetSerAl's other examples, @()
's behavior with New-Object
-created arrays and collections - may be surprising - see below.
The purpose of @(...)
and how it works:
The purpose of @()
, the array-subexpression operator, is, loosely speaking, to ensure that the result of an expression/command is treated as an array, even if it happens to be a scalar (single object).
More accurately, @()
behaves as follows: Tip of the hat to PetSerAl for his extensive help.
In PSv5.1+, using an expression that directly constructs an array using ,
(the array expression operator) optimizes @()
away:
E.g., @(1, 2)
is the same as just 1, 2
, and @(, 1)
is the same as just , 1
.
In the case of an array constructed with just ,
- which yields a System.Object[]
array - this optimization is helpful, because it saves the unnecessary step of first unrolling that array and then repackaging it (see below).
Presumably, this optimization was prompted by the widespread and previously inefficient practice of using @( ..., ..., ...)
to construct arrays, stemming from the mistaken belief that @()
is needed to construct an array.
However, in Windows PowerShell v5.1 only the optimization is also applied when constructing an array with a specific type using a cast, such as [int[]]
(the behavior has been corrected in PowerShell Core and older Windows PowerShell versions are not affected); e.g.,
@([int[]] (1, 2)).GetType().Name
yields Int32[]
. This is the only situation in which @()
returns something other than System.Object[]
, and assuming that it always does can lead to unexpected errors and side effects; e.g.:
@([int[]] (1, 2))[-1] = 'foo'
breaks.
$a = [int[]] (1, 2); $b = @([int[]] $a)
unexpectedly doesn't create a new array - see this GitHub issue.
Otherwise: If the (first) statement inside @(...)
is an expression, it is collected using PowerShell's standard collection-unrolling (unwrapping) technique; a command's output is interpreted as-is, however; in either case the resulting count of objects determines the behavior:
If the result is a single item / contains no items, the result is wrapped in a single-element / empty array of type [System.Object[]]
.
E.g., @('foo').GetType().Name
yields Object[]
and @('foo').Count
yields 1
(though, as stated, in PSv3+, you can use 'foo'.Count
directly).
@( & { } ).Count
yields 0
(executing an empty script block outputs a "null collection"
([System.Management.Automation.Internal.AutomationNull]::Value
)
Caveat: @()
around a New-Object
call that creates an array / collection outputs that array/collection wrapped in a single-element outer array.
@(New-Object System.Collections.ArrayList).Count
yields 1
- the empty array list is wrapped in a single-element System.Object[]
instance.
The reason is that New-Object
, by virtue of being a command (such as a cmdlet call) is not subject to unwrapping, causing @()
to see only a single item (which happens to be an array/collection), which it therefore wraps in a single-item array.
What may be confusing is that this does not happen when you use an expression to construct an array / a collection, because the expression's output is unwrapped (unrolled):
@([system.collections.arraylist]::new()).Count
yields 0
; the expression outputs an empty collection that is unrolled to a result containing no items, which @()
repackaged as an empty System.Object[]
array.
Note that, in PSv3+, simply using an extra set of parentheses ((...)
) with New-Object
- which converts the New-Object
command to an expression - would yield the same result:
@((New-Object System.Collections.ArrayList)).Count
yields 0
too.
If the result comprises multiple items, these items are returned as an array, (typically, but in WinPSv5.1 not always - see above) as a regular PowerShell array ([System.Object[]]
):
- E.g.,
$arr = [int[]] (1, 2); @($arr)
unrolls [int[]]
array $arr
and then repackages the elements as a System.Object[]
array.
(As discussed above, in WinPSv5.1 only, if you place the array-creation expression directly inside @()
, you actually get an [int[]]
array.)