9

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?

mklement0
  • 382,024
  • 64
  • 607
  • 775
CobyCode
  • 193
  • 5
  • 2
    `$Result = @(Your command here)` – user4003407 Jul 13 '17 at 19:53
  • 4
    `(Get-Services).Count` works because `Get-Services` returns multiple objects. If you need an array (of potentially 0 or 1 object(s)), then use the array sub-expression operator (`@(...)`) as suggested by @PetSerAl above – Mathias R. Jessen Jul 13 '17 at 19:56
  • @PetSerAl Thanks. Isn't that exactly the same as [Array]$Result = Command_Here ? – CobyCode Jul 13 '17 at 19:57
  • @MathiasR.Jessen Thanks! I was under the impression that pretty much everything in powershell was setup to handle multiple objects? Even "(10*2).Count" works. What determines whether the object built can handle mutliple or single objects? – CobyCode Jul 13 '17 at 20:04
  • 5
    @CobyCode No, them are different: `[Array]$a = &{}; $b = @(&{}); $a.GetType(); $b.GetType()` or `[Array]$a = New-Object Object[] 10; $b = @(New-Object Object[] 10); $a.Count; $b.Count` or `[Array]$a = New-Object Collections.Generic.List[Object]; $b = @(New-Object Collections.Generic.List[Object]); $a.Count; $b.Count`. – user4003407 Jul 13 '17 at 20:10
  • @PetSerAl That took me a while to figure out! I also found some further information from this question [link](https://stackoverflow.com/questions/42355649/array-types-in-powershell-system-object-vs-arrays-with-specific-types) which it looks like you also had a hand in answering. I can see that they are different, but still not entirely sure what the difference is. .GetType() shows a basetype of system.array for both of them. Also: Can you write up as an answer so I can give you credit? – CobyCode Jul 13 '17 at 21:05

2 Answers2

15

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:

  • Using this intrinsic (engine-supplied) .Count property unexpectedly fails when Set-StrictMode-Version 2 or higher is in effect, which is a long-standing bug reported in GitHub issue #2798, still present as of PowerShell 7.2 (whereas a type-native .Count property, such as on an array, can safely be accessed).

  • ([pscustomobject] instances not playing by these rules was a known bug, fixed in 2017).

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:

  • as a workaround for the bug at hand / the Set-StrictMode -Version 2 bug.

  • in case a scalar object happens to have its own .Count property.


Generally,

  • If you do need to ensure that output from a command is captured as an array, @(...) is the PowerShell-idiomatic, more concise, and syntactically easier form compared to [Array] ... / [object[]] ...

  • By contrast, capturing output from an expression may call for [Array] ... / [object[]] ..., to avoid the potential inefficiency of enumerating expression output that already is an array and collecting it in a new array - see below for details.

  • With either type of output, as in your question, you may use [Array] as a type constraint on a variable (placed to the left of it), so as to ensure that later assignments too are treated as arrays; a contrived example: [Array] $a = 1 stores 1 wrapped in a (single-element) [object[]] array, and a later $a = 2 assignment implicitly does that for 2.

Additionally, @(...) and [Array] ... are typically, but not always 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).:

  • @(...) collects an enclosed command's output as-is / an enclosed expression's enumerated output in an - always new - [object[]] array, even if there's only a single output object.

    • As such, @() is merely a slight modification - for the single-object output case - of PowerShell's default behavior with respect to collecting command and expression output from its success output stream - see this answer for more information. In short, single-object output is by default collected as-is, whereas @() wraps it in an array (by contrast, multiple output objects always get collected in an array, in both cases, of necessity).Tip of the hat to Slawomir Brzezinski for helping to clarify.
  • @(...) is never needed for array literals (in v5.1+ it is optimized away) - use of ,, the array constructor operator by itself is generally sufficient, e.g., 'foo', 'bar' instead of @('foo', 'bar') - but you may prefer it for visual clarity; it is also useful in the following cases:

    • to create an empty array: @())

    • to create a single-element array - e.g. @('foo') - which is easier to read than the unary form of , that would otherwise be required - e.g. , 'foo'

    • for syntactic convenience: to spread what is conceptually an array literal across multiple lines without having to use , to separate the elements and without having to enclose commands in (...); e.g.:

      @(
        'one'
        Write-Output two
      )
      
  • Pitfalls:

    • @() is not an array constructor, but a "guarantor": therefore, @(@(1,2)) does not create a nested array:

      • @(@(1, 2)) is effectively the same as @(1, 2) (and just 1, 2). In fact, each additional @() is an expensive no-op, because it simply creates a copy of the array output by the previous one.
      • Use the unary form of , the array constructor operator, to construct nested arrays:
        , (1, 2)
    • $null is considered a single object by @(), and therefore results in a single-element array with element $null:

      • @($null).Count is 1
    • Command calls that output a single array as a whole result in a nested array:

      • @(Write-Output -NoEnumerate 1, 2).Count is 1
    • In an expression, wrapping a collection of any type in @() enumerates it and invariably collects the elements in a (new) [object[]] array:

      • @([System.Collections.ArrayList] (1, 2)).GetType().Name returns 'Object[]'

Read on for more detailed information, if needed.


Details:

@() behaves as follows: Tip of the hat to PetSerAl for his extensive help.

  • In PSv5.1+ (Windows PowerShell 5.1 and PowerShell [Core] 6+), using an expression that directly constructs an array using ,, the array constructor 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 unexpectedly also applied when constructing an array with a specific type using a cast, such as [int[]] (the behavior has been corrected in PowerShell [Core] 6+ 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 GitHub issue #4280.

  • Otherwise: If the (first) statement inside @(...) is an expression that happens to be an enumerable,[1] it is enumerated, i.e. its elements are sent one by one to the success output stream; a command's (typically one-by-one streaming) output is collected as-is; 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 outputs the list as a single object (that just so happens to be an enumerable itself), array/collection), causing @() to wrap it 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 enumerated (an operation sometimes also called unwrapping or unrolling):

          • @([system.collections.arraylist]::new()).Count yields 0; the expression outputs an empty collection that is enumerated, and since there's noting to enumerate, @() creates 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 a regular PowerShell array ([System.Object[]]); e.g.:

      • With a command:

        • $arr = @(Get-ChildItem *.txt) collects the one-by-one streaming output from Get-ChildItem in a System.Object[] array
      • With an expression:

        • $arr = [int[]] (1, 2); @($arr) enumerates [int[]] array $arr and then repackages the elements as a System.Object[] array.

        • Note the inefficiency and potential loss of type fidelity of this process: the original array is enumerated and collected in a new array that is always of type System.Object[]; the efficient alternative is to cast to [array] (which also works with commands): [array] $result = ...


[1] For a summary of which types PowerShell considers enumerable - which both excludes select types that do implement IEnumerable and includes one that doesn't - see the bottom section of this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks! That makes sense. But then what is the preferred way to get around this? If I use $a = @(...) , later, when I have use the value of $a, it doesn't work when I am using it as an input on some other cmldet that is expecting a system.object[] ... so '$disk = @(get-disk); new-volume $disk' will fail. I guess I could use 'new-volume $disk[0]' or '$disk=disk[0]; new-volume $disk' .... – CobyCode Jul 14 '17 at 01:07
  • Also, to note, I seem to be getting this for all cmdlets that are built upon [Microsoft.Management.Infrastructure.CimInstance] – CobyCode Jul 14 '17 at 01:40
  • 1
    @CobyCode: `$disk = @(get-disk)` indeed returns a `[System.Object[]]` instance, which you can verify with `Get-Member -InputObject $disk`. From what I can tell, `New-Volume`'s first positional parameter is a _scalar_ type, so trying to pass it a `[System.Object[]]` instance is expected to fail; see `New-Volume -?`. – mklement0 Jul 14 '17 at 17:31
  • 1
    @mklement0 This might be just the most clearly written comment I have ever seen (with the exception of your other answer, [here](https://stackoverflow.com/questions/42355649/array-types-in-powershell-system-object-vs-arrays-with-specific-types/42373523#42373523) . Thank you! As per your suggestion, I am going to submit this to uservoice. – CobyCode Jul 14 '17 at 17:50
  • @SlawomirBrzezinski, to bring closure to this exchange: Thank you for encouraging me to improve the answer and for bringing clarity to how `@()` is related to the default success output-stream behavior. Given the focus of this answer, I think my update is the appropriate way to reflect this (I just updated to give you a little shout-out). If you agree, I suggest we clean up our comments here (I've already removed my previous ones). – mklement0 Mar 28 '22 at 16:35
  • @SlawomirBrzezinski, thanks, but I think the slight-modification angle is the better way to frame it, because that's exactly what it is: it builds on the default behavior by wrapping collected output that comprises a single object only in an array (too). That is, it doesn't matter whether that single object happens to be an enumerable or not, which contradicts your comment, if I read it correctly. – mklement0 Mar 28 '22 at 17:52
  • As for `, $array` vs. `@(Write-Output -NoEnumerate $array)`: Performance differences and [other weirdness resulting from _cmdlet_ use](https://github.com/PowerShell/PowerShell/issues/5579) aside, this comes down to a matter of taste. Yes, unary `,` is obscure _at first_, but on a general note I don't think language features should be avoided just because they may be unfamiliar to users coming from other languages. – mklement0 Mar 28 '22 at 18:13
  • Let's continue the discussion in the [chat](https://chat.stackoverflow.com/rooms/243389/discussion-between-slawomir-brzezinski-and-mklement0) you've previously tried to initiate. – mklement0 Mar 28 '22 at 18:32
2

@mklement0 has a great answer, but one thing to add: if your script (or the script calling yours) has Set-StrictMode, the automatic .Count and .Length properties stop working:

$ Set-StrictMode -off
$ (42).Length
1
$ Set-StrictMode -Version Latest
$ (42).Length
ParentContainsErrorRecordException: The property 'Length' cannot be found on this object. Verify that the property exists.

To be safe, you can wrap any unknown variable in an array @(...) before checking the length:

$ $result = Get-Something
$ @($result).Length
1
Carl Walsh
  • 6,100
  • 2
  • 46
  • 50
  • Thanks, and good point - I've also updated my answer. This bug has been around for a long time, and was notably filed by Jeffrey Snover himself - see [GitHub issue #2798](https://github.com/PowerShell/PowerShell/issues/2798). – mklement0 Mar 31 '21 at 16:51