问题
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