Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparison and operation statement syntax does not apply member enumeration (ForEach-Object, Where-Object) #9576

Open
mklement0 opened this issue May 10, 2019 · 9 comments

Comments

Projects
None yet
4 participants
@mklement0
Copy link
Contributor

commented May 10, 2019

Note: This problem may not often arise in practice, but there is no good reason for this inconsistency.

The simplified syntax for Where-Object and ForEach-Object introduced in v3 - called comparison and operation statement respectively - is meant to be just syntactic sugar that makes for less "noisy" commands, due to not requiring a script block and operation on $_ being implied.

However, in the case of input objects that are themselves collections, the behaviors differ: script-block syntax performs member enumeration, whereas the simplified syntax does not.

That is, a given property Foo isn't automatically looked for on the elements of an input object, if it happens to be a collection, unlike when $_.Foo is used in a script block.

Steps to reproduce

Run the following Pester tests:

Describe "Both script-block and operation/comparison-statement syntax perform member enumeration." {
  BeforeAll {
    $arr = ([pscustomobject] @{ prop = 1 }), ([pscustomobject] @{ prop = 2 })
  }
  It "ForEach-Object" {
    $expected = 1, 2
    # Note how $arr is sent as a *single object* through the pipeline
    # and how the output should be a single array of the property values, 
    # courtesy of member enumeration.
    , $arr | ForEach-Object { $_.prop } | Should -Be $expected
    , $arr | ForEach-Object prop | Should -Be $expected    
  }
  It "Where-Object" {
    # Note that the entire input array should be output as-is, as a single object.
    , $arr | Where-Object { $_.prop -eq 1 } | Should -HaveCount 1
    , $arr | Where-Object prop -eq 1 | Should -HaveCount 1
  }
}

Expected behavior

All tests should pass.

Actual behavior

The commands using operation / comparison syntax fail, because they don't apply member enumeration.

Environment data

PowerShell Core 6.2.0
@msftrncs

This comment has been minimized.

Copy link

commented May 16, 2019

The behavior changes if you remove the array operator , from in front of $args in the pipeline. I think this makes the pipeline an [array[array[]], so you are only seeing it process the first array level. I think the issue is with what the , operator does, and not with the simplified syntax.

@msftrncs

This comment has been minimized.

Copy link

commented May 16, 2019

Try this: (using $arr above)

, $arr | foreach {$_.gettype()} # returns one type
$arr | foreach {$_.gettype()} # returns two types
@vexx32

This comment has been minimized.

Copy link
Contributor

commented May 16, 2019

That's precisely the point. Typically when you pass an array to a comparison operator, the comparison also enumerates the array. For example.

PS> @(1, 2, 3, 4) -gt 2
3
4

The Where-Object simplified syntax implementation seems to avoid this somehow, creating inconsistent results.

@msftrncs

This comment has been minimized.

Copy link

commented May 16, 2019

Yes, but , @(1, 2, 3, 4) -gt 2 will fail … its not comparable. Adding the comma operator in front of an array invalidates the logic because the array is now contained in another array.

The simplified syntax is working correctly, the scriptblock syntax is somehow breaking down the array of an array.

@vexx32

This comment has been minimized.

Copy link
Contributor

commented May 16, 2019

Yes to the first point

As for the second... not true. The pipeline is breaking down the outer array, as designed, and the operator is behaving as it ought. The fact that the cmdlet obscures this and breaks the operators' behaviour from the norm is an abnormality.

For example:

$Array = @( 1, 2, 3, 4 )

, $Array | ForEach-Object { $_ -lt 4 }

The scriptblock syntax is working as it should. The simplified syntax is modifying how the operators actually behave.

@msftrncs

This comment has been minimized.

Copy link

commented May 16, 2019

The pipeline doesn't break down the array, the Where-Object command breaks down the array in its process block. The pipeline passes the first collection item, which only contains one item. Where-Object's simplified syntax is then only able to see one object, and that object does not contain the member you have requested. The simplified syntax only works against members of the object, where as the scriptblock version is able to access the original pipeline object (iterated of course), for which now the scriptblock's operator is able to further iterate.

@vexx32

This comment has been minimized.

Copy link
Contributor

commented May 16, 2019

I see what you're saying, but PowerShell typically applies automatic enumeration when you request a property value that doesn't exist on the collection itself. For example:

$array = @(
    [PSCustomObject]@{ a = 1; b = 3 }
    [PSCustomObject]@{ a = 4; b = -1 }
)
$array.b
#  outputs
3
-1

The cmdlet isn't applying this rather standard PS logic to a collection that is received over the pipeline.

@msftrncs

This comment has been minimized.

Copy link

commented May 16, 2019

Just to be correct in the examples so far,

(, $array).b
(, $array).count # verify only 1 object at this point

Which does work. In some ways though I think this is flawed and should not work. At the point here where I am requesting property/member b, it doesn't exist, because I am testing an array of an array of hashtable/custom object's. I should need to deliberately iterate the first level objects to get to properties stored in the second level. I should have to do this, for consistency.

Say for instance I had a list of lists. Which way would be the right behavior there? Should [array] have a special behavior as it mostly does now?

@vexx32

This comment has been minimized.

Copy link
Contributor

commented May 16, 2019

I don't think this issue is necessarily the right place to argue against the well established paradigm of member enumeration; it's been around for a long time now, and this issue is about bringing nonconformant parts of PS into line such that they behave as one might expect compared to alternate but generally equivalent methods.

If you had a list of lists, the member enumeration would generally be non functional. Since the top level list has all the properties that the second-level lists do, it would mask all their properties. Items stored in each of the second level lists would be masked from normal member enumeration until you start unwrapping the top level collection, e.g., with a pipeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.