Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell Where-Object vs. Where method

Tags:

powershell

An interesting and weird thing I noticed writing PowerShell classes lines:

class A {

    [object] WhereObject(){
        return @(1,2) | Where-Object {$_ -gt 2}
    }

    [object] Where(){
        return @(1,2).Where( {$_ -gt 2})
    }
}

$a = new-object A
$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.

$a.Where() # Works well

It looks like it is by design. Why does it work so?

Workaround

Function which explicitly convert "empty" value to $null:

function Get-NullIfEmpty {
   param(
       [Parameter(ValueFromPipeline=$true)][array] $CollectionOrEmtpy
   )

   begin { $output = $null }

   process
   {
      if($output -eq $null -and $CollectionOrEmtpy -ne $null){
          $output = @()
      }
      foreach ($element in $CollectionOrEmtpy)
      {
          $output += $element
      }
   }

   end { return $output }
}

In this case, the method will look like:

[object] WhereObject() {
   return @(1,2) | Where-Object {$_ -gt 2} | Get-NullIfEmpty
}

I tried to return an empty array from the class method, but it is also tricky because for a regular function an empty array means "nothing" as well. If you have a call chain like method1 -> function -> method2 - method1 throw the same exception. Because the function converts an empty array to nothing.

So converting to $null is optimal in my case :)

like image 657
Ilya Avatar asked Jun 20 '18 20:06

Ilya


2 Answers

  • The (PowerShell v4+) .Where() method, which is evaluated in expression mode, always returns an instance of [System.Collections.ObjectModel.Collection[psobject]]:

    • If no input objects match, that instance is simply empty (it has no elements and its .Count property returns 0).
  • By contrast, the Where-Object cmdlet uses pipeline semantics, which implies the following output behavior:

    • If nothing is output (if nothing matches the filter script block), the return value is a "null collection", which is technically the [System.Management.Automation.Internal.AutomationNull]::Value singleton.

    • If a single item matches, that item is output as-is.

    • If multiple items match and they are collected in a variable / evaluated as part of an expression, they are collected in an [object[]] array.


As for the specific symptom - which Bruce Payette's answer has since confirmed to be a bug.

  • Update: The bug is fixed since at least v7; returning "nothing" (AutomationNull) is now coerced to $null; see the original bug report on GitHub.

An internal [List[object]] instance is used to collect the method call's output, executed via an internal pipeline. If that internal pipeline outputs "nothing" - i.e., [System.Management.Automation.Internal.AutomationNull]::Value - no object is added to the list. However, subsequent code assumes that there is at least one object in the list and blindly accesses index 0, causing the error at hand.

A simpler reproduction of the problem:

class A {
  # Try to return [System.Management.Automation.Internal.AutomationNull]::Value
  # (which is what `& {}` produces).
  [object] WhereObject(){ return & {} }
}

$a = new-object A

$a.WhereObject() # Throw exception Index was out of range. Must be non-negative and less than the size of the collection.

As for the desirable behavior:

It seems that the fix will result in $null getting output if the method's code returns the "null collection", using C#'s default-value feature - see this comment.

like image 128
mklement0 Avatar answered Oct 14 '22 07:10

mklement0


The .Where() operator always returns a Collection<PSObject>. The pipeline case however, returns nothing. This is a problem because the code that invokes the scriptblock expects there to be an object in the result List i.e. result.Count == 1. There are no objects in the pipeline case so you get an index-out-of-range error. So this is a bug. We should still generate an error but it should be "non-void methods must return a value" or some such. BTW - the code in question is here.

like image 38
Bruce Payette Avatar answered Oct 14 '22 08:10

Bruce Payette