2

Trying to make a script that request more info (group Id) if there are SCOM groups with identical names:

function myFunction {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$true)]
        [string[]]$ObjectName
    )

    foreach ($o in $ObjectName) {
        $p = Get-SCOMGroup -DisplayName "$o" | select DisplayName
        <#
        if ($p contains more than one string) {
            "Request group Id"
        } else {
            "do this"
        }
        #>            
    }
}

Need help with the functionality in the comment block.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Freshman
  • 293
  • 3
  • 8
  • 19

2 Answers2

3

Wrap the value in an array subexpression @() and count how many entries it has:

if(@($p).Count -gt 1){"Request group Id"}
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
1

Note: This answer complements Mathias R. Jessen's helpful answer.

Counting the number of objects returned by a command:

  • Mathias' answer shows a robust, PowerShell v2-compatible solution based on the array sub-expression operator, @().

      # @() ensures that the output of command ... is treated as an array,
      # even if the command emits only *one* object.
      # You can safely call .Count (or .Length) on the result to get the count.
      @(...).Count 
    
  • In PowerShell v3 or higher, you can treat scalars like collections, so that using just (...).Count is typically enough. (A scalar is a single objects, as opposed to a collections of objects).

      # Even if command ... returns only *one* object, it is safe
      # to call .Count on the result in PSv3+
      (...).Count
    

These methods are typically, but not always interchangeable, as discussed below.


  • Choose @(...).Count, if:

    • you must remain PSv2-compatible
    • you want to count output from multiple commands (separated with ; or newlines)
    • for commands that output entire collections as a single object (which is rare), you want to count such collections as 1 object.[1]
    • more generally, if you need to ensure that the command output is returned as a bona fide array, though note that it is invariably of type [object[]]; if you need a specific element type, use a cast (e.g., [int[]]), but note that you then don't strictly need the @(...); e.g.,
      [int[]] (...) will do - unless you want to prevent enumeration of collections output as single objects.
  • Choose (...).Count, if:

    • only one command's output must be counted
    • for commands that output entire collections as a single object, you want to count the individual elements of such collections; that is, (...) forces enumeration of command output.[2]
    • for counting the elements of commands's output already stored in a variable - though, of course, you can then simply omit the (...) and use $var.Count

Caveat: Due to a longstanding bug (still present as of PowerShell Core 6.2.0), accessing .Count on a scalar fails while Set-StrictMode -Version 2 or higher is in effect - use @(...) in that case, but note that you may have to force enumeration.

To demonstrate the difference in behavior with respect to (rare) commands that output collections as single objects:

PS> @(Write-Output -NoEnumerate (1..10)).Count 
1  # Array-as-single-object was counted as *1* object

PS> (Write-Output -NoEnumerate (1..10)).Count 
10  # Elements were enumerated.

Performance considerations:

  • If a command's output is directly counted, (...) and @(...) perform about the same:

      $arr = 1..1e6  # Create an array of 1 million integers.
    
      { (Write-Output $arr).Count }, { @(Write-Output $arr).Count } | ForEach-Object {
        [pscustomobject] @{
          Command = "$_".Trim()
          Seconds = '{0:N3}' -f (Measure-Command $_).TotalSeconds
        }
      }
    
    • Sample output, from a single-core Windows 10 VM (the absolute timings aren't important, only that the numbers are virtually the same):

          Command                    Seconds
          -------                    -------
          (Write-Output $arr).Count  0.352
          @(Write-Output $arr).Count 0.365
      
  • By contrast, for large collections already stored in a variable, @(...) introduces substantial overhead, because the collection is recreated as a (new) array (as noted, you can just $arr.Count):

      $arr = 1..1e6  # Create an array of 1 million integers.
    
      { ($arr).Count }, { @($arr).Count } | ForEach-Object {
        [pscustomobject] @{
          Command = "$_".Trim()
          Seconds = '{0:N3}' -f (Measure-Command $_).TotalSeconds
        }
      }
    
    • Sample output; note how the @(...) solution is about 7 times slower:

          Command       Seconds
          -------       -------
          ($arr).Count  0.009
          @($arr).Count 0.067
      

Coding-style considerations:

The following applies in situations where @(...) and (...) are functionally equivalent (and either perform the same or when performance is secondary), i.e., when you're free to choose which construct to use.

Mathias recommends @(...).Count, stating in a comment:

There's another reason to explicitly wrap it in this context - conveying intent, i.e., "We don't know if $p is a scalar or not, hence this construct".

My vote is for (...).Count:

Once you understand that PowerShell (v3 or higher) treats scalars as collections with count 1 on demand, you're free to take advantage of that knowledge without needing to reflect the distinction between a scalar and an array in the syntax:

  • When writing code, this means you needn't worry about whether a given command situationally may return a scalar rather than a collection (which is common in PowerShell, where capturing output from a command with a single output object captures that object as-is, whereas 2 or more output objects result in an array).

  • As a beneficial side effect, the code becomes more concise (and sometimes faster).

Example:

# Call Get-ChildItem twice, and, via Select-Object, limit the 
# number of output objects to 1 and 2, respectively.
1..2 | ForEach-Object {

  # * In the 1st iteration, $var becomes a *scalar* of type [System.IO.DirectoryInfo]
  # * In the 2nd iteration, $var becomes an *array* with 
  #   2 elements of type [System.IO.DirectoryInfo]
  $var = Get-ChildItem -Directory / | Select-Object -First $_

  # Treat $var as a collection, which in PSv3+ works even
  # if $var is a scalar:
  [pscustomobject] @{
    Count = $var.Count
    FirstElement = $var[0]
    DataType = $var.GetType().Name
  }

}

The above yields:

Count FirstElement  DataType
----- ------------  --------
    1 /Applications DirectoryInfo
    2 /Applications Object[]

That is, even the scalar object of type System.IO.DirectoryInfo reported its .Count sensibly as 1 and allowed access to "its first element" with [0].

For more about the unified handling of scalars and collections, see this answer.


[1] E.g., @(Write-Output -NoEnumerate 1, 2).Count is 1, because the Write-Output command outputs a single object - the array 1, 2 - _as a whole. Because only a single object is output, @(...) wraps that object in an array, resulting in , (1, 2), i.e. a single-element array whose first and only element is itself an array.

[2] E.g., (Write-Output -NoEnumerate 1, 2).Count is 2, because even though the Write-Output command outputs the array as a single object, that array is used as-is. That is, the whole expression is equivalent to (1, 2).Count. More generally, if a command inside (...) outputs just one object, that object is used as-is; if it outputs multiple objects, they are collected in a regular PowerShell array (of type [object[]]) - this is the same behavior you get when capturing command output via a variable assignment ($captured = ...).

mklement0
  • 382,024
  • 64
  • 607
  • 775