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

后端 未结 2 1448
既然无缘
既然无缘 2021-01-15 12:30

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

Set-StrictMode -Version Latest

$x = \"[]\" | ConvertFrom-Json | Where { $_.na         


        
相关标签:
2条回答
  • 2021-01-15 13:12

    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 (see this GitHub issue for the associated discussion).

    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 (...), the grouping operator (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).

    0 讨论(0)
  • 2021-01-15 13:15

    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? :-)

    0 讨论(0)
提交回复
热议问题