Why is PowerShell applying the predicate of a `Where` to an empty list

廉价感情. 提交于 2020-08-19 09:06:27

问题


If I run this in PowerShell, I expect to see the output 0 (zero):

Set-StrictMode -Version Latest

$x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
Write-Host $x.Count

Instead, I get this error:

The property 'name' cannot be found on this object. Verify that the     property exists and can be set.
At line:1 char:44
+     $x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
+                                            ~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

If I put braces around "[]" | ConvertFrom-Json it becomes this:

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
Write-Host $y.Count

And then it "works".

What is wrong before introducing the parentheses?

To explain the quotes around "works" - setting strict mode Set-StrictMode -Version Latest indicates that I call .Count on a $null object. That is solved by wrapping in @():

$z = @(("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" })
Write-Host $z.Count

I find this quite dissatisfying, but it's an aside to the actual question.


回答1:


Why is PowerShell applying the predicate of a Where to an empty list?

Because ConvertFrom-Json tells Where-Object to not attempt to enumerate its output.

Therefore, PowerShell attempts to access the name property on the empty array itself, much like if we were to do:

$emptyArray = New-Object object[] 0
$emptyArray.name

When you enclose ConvertFrom-Json in parentheses, powershell interprets it as a separate pipeline that executes and ends before any output can be sent to Where-Object, and Where-Object can therefore not know that ConvertFrom-Json wanted it to treat the array as such.


We can recreate this behavior in powershell by explicitly calling Write-Output with the -NoEnumerate switch parameter set:

# create a function that outputs an empty array with -NoEnumerate
function Convert-Stuff 
{
  Write-Output @() -NoEnumerate
}

# Invoke with `Where-Object` as the downstream cmdlet in its pipeline
Convert-Stuff | Where-Object {
  # this fails
  $_.nonexistingproperty = 'fail'
}

# Invoke in separate pipeline, pass result to `Where-Object` subsequently
$stuff = Convert-Stuff
$stuff | Where-Object { 
  # nothing happens
  $_.nonexistingproperty = 'meh'
}

Write-Output -NoEnumerate internally calls Cmdlet.WriteObject(arg, false), which in turn causes the runtime to not enumerate the arg value during parameter binding against the downstream cmdlet (in your case Where-Object)


Why would this be desireable?

In the specific context of parsing JSON, this behavior might indeed be desirable:

$data = '[]', '[]', '[]', '[]' |ConvertFrom-Json

Should I not expect exactly 5 objects from ConvertFrom-Json now that I passed 5 valid JSON documents to it? :-)




回答2:


With an empty array as direct pipeline input, nothing is sent through the pipeline, because the array is enumerated, and since there's nothing to enumerate - because an empty array has no elements - the Where (Where-Object) script block is never executed:

Set-StrictMode -Version Latest

# The empty array is enumerated, and since there's nothing to enumerate,
# the Where[-Object] script block is never invoked.
@() | Where { $_.name -eq "Baz" } 

By contrast, in PowerShell versions up to v6.x "[]" | ConvertFrom-Json produces an empty array as a single output object rather than having its (nonexistent) elements enumerated, because ConvertFrom-Json in these versions doesn't enumerate the elements of arrays it outputs; it is the equivalent of:

Set-StrictMode -Version Latest

# Empty array is sent as a single object through the pipeline.
# The Where script block is invoked once and sees $_ as that empty array.
# Since strict mode is in effect and arrays have no .name property
# an error occurs.
Write-Output -NoEnumerate @() | Where { $_.name -eq "Baz" }

ConvertFrom-Json's behavior is surprising in the context of PowerShell - cmdlets generally enumerate multiple outputs - but is defensible in the context of JSON parsing; after all, information would be lost if ConvertFrom-Json enumerated the empty array, given that you wouldn't then be able to distinguish that from empty JSON input ("" | ConvertFrom-Json).

The consensus was that both use cases are legitimate and that users should have a choice between the two behaviors - enumeration or not - by way of a switch.

Therefore, starting with PowerShell [Core] 7.0:

  • Enumeration is now performed by default.

  • An opt-in to the old behavior is available via the new -NoEnumerate switch.

In PowerShell 6.x-, if enumeration is desired, the - obscure - workaround is to force enumeration by simply enclosing the ConvertFrom-Json call in (...) (which converts it to an expression, and expressions always enumerate a command's output when used in the pipeline):

# (...) around the ConvertFrom-Json call forces enumeration of its output.
# The empty array has nothing to enumerate, so the Where script block is never invoked.
("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }

As for what you tried: your attempt to access the .Count property and your use of @(...):

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
$y.Count # Fails with Set-StrictMode -Version 2 or higher

With the ConvertFrom-Json call wrapped in (...), your overall command returns "nothing": loosely speaking, $null, but, more accurately, an "array-valued null", which is the [System.Management.Automation.Internal.AutomationNull]::Value singleton that indicates the absence of output from a command. (In most contexts, the latter is treated the same as $null, though notably not when used as pipeline input.)

[System.Management.Automation.Internal.AutomationNull]::Value doesn't have a .Count property, which is why with Set-StrictMode -Version 2 or higher in effect, you'll get a The property 'count' cannot be found on this object. error.

By wrapping the entire pipeline in @(...), the array subexpression operator, you ensure treatment of the output as an array, which, with array-valued null output, creates an empty array - which does have a .Count property.

Note that you should be able to call .Count on $null and [System.Management.Automation.Internal.AutomationNull]::Value, given that PowerShell adds a .Count property to every object, if not already present - including to scalars, in a commendable effort to unify the handling of collections and scalars.

That is, with Set-StrictMode set to -Off (the default) or to -Version 1 the following does work and - sensibly - returns 0:

# With Set-StrictMode set to -Off (the default) or -Version 1:

# $null sensibly has a count of 0.
PS> $null.Count
0

# So does the "array-valued null", [System.Management.Automation.Internal.AutomationNull]::Value 
# `. {}` is a simple way to produce it.
PS> (. {}).Count # `. {}` outputs 
0

That the above currently doesn't work with Set-StrictMode -Version 2 or higher (as of PowerShell [Core] 7.0), should be considered a bug, as reported in this GitHub issue (by Jeffrey Snover, no less).



来源:https://stackoverflow.com/questions/55575501/why-is-powershell-applying-the-predicate-of-a-where-to-an-empty-list

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!