Powershell: Are the rules for conditional enumeration the same for comparison operators and the pipeline?

Created on 9 Jan 2018  路  7Comments  路  Source: PowerShell/PowerShell

Consider the following treatments of $x:

$x -eq $RHS
$x -contains $RHS

function f { param([Parameter(ValueFromPipeline)]$y) process{$y} }
$x | f

function g { param([Parameter(Position=1)]$y) $y }
g $x

There are conditions of $x which results in PowerShell enumerating $x for each of these treatments. Setting $x=1,2, for example, causes $x to be enumerated by -eq and -contains, at the input to f, and at the output of g. On the other hand, setting $x='12', results in no enumeration.

  1. Are the rules that decide whether or not $x is enumerated the same for the pipeline and those comparison operators that perform conditional enumeration?
  2. The value of [LanguagePrimitives]::GetEnumerator($x) _seems_ to be non-null iff $x gets enumerated by those treatments. Is this a mere coincidence, a rule PowerShell aspires to, or true by construction?
  3. Are there other (user-observable) parts of the language (aside from operators and the pipeline) that perform conditional enumeration? If so, are the rules used by those parts the same?
Issue-Discussion Resolution-Answered

All 7 comments

  1. The rules are intended to be the same.

    For operators, the rules are implemented here, and I apologize that the code is not necessarily easy to understand.

    For the pipeline, the decision is made by LanguagePrimitives.GetEnumerator, you can see the call here.

  2. True by construction for pipelines, rule to aspire to for operators.

  3. Parameter binding comes to mind - e.g. when to bind to a parameter of type object versus object[]. To the best of my knowledge and memory, operators all rely on PSEnumerableBinder.IsEnumerable (there are many uses) and everything else relies on LanguagePrimitives.GetEnumerator.

Thanks @lzybkr. With respect to parameter binding, do you mean that

function f {
    [CmdletBinding(DefaultParameterSetName='scalar')]
    param
    (
        [Parameter(ParameterSetName = 'scalar',
                   Position = 1)]
        [int]
        $Scalar,

        [Parameter(ParameterSetName = 'array',
                   Position = 1)]
        [int[]]
        $Array
    )
    return $PSCmdlet.ParameterSetName
}

f 1
f 1,2
f ([System.Collections.ArrayList]@(1,2))
f ([System.Collections.Queue]@(1,2))

decides on the parameter set based on the result of LanguagePrimitives.GetEnumerator for the argument?

The first three calls to f seem consistent with that. However, f ([System.Collections.Queue]@(1,2)) tries (and fails) to bind to $Array despite non-null LP.GetEnumerator for the argument. Trace-Command mentions "Argument type Queue is not IList, treating this as scalar" in that case. That business seems to happen in EncodeCollection which seems to try to convert the argument to an array. I didn't spot a call to GetEnumerator leading up to or in EncodeCollection, however. It seems like this decision hinges on whether the argument is IList rather than LP.GetEnumerator. Is that interpretation correct?

Right - I forgot that it's not quite the same - I don't know the reason why though.

@lzybkr

For operators, the rules are implemented here, and I apologize that the code is not necessarily easy to understand.
...
True by construction for pipelines, rule to aspire to for operators.

Do these statements also apply to the . (property) operator? That operator has some behavior whose rules I haven't quite worked out. It seems to enumerate according to LP.GetEnumerator, but only in some cases. For example,

$x = '1','2'

$x.Chars
$x.Length

enumerates $x in $x.Chars but not $x.Length. Can you shed some light on this?

For property access or method invocation, if the object has the property/method, no enumeration takes place.

Count and Length are special - they are assumed to exist on all objects - if the object has the property, it is used, otherwise the sane value is returned (0 for null, 1 otherwise).

Ok. That seems to explain the behavior for all properties and methods I've tested except Length. The behavior for Length seems less like Count and more like other properties. Consider the following:

$y = [System.Collections.Queue]@('123','1234')
$y.Length
$y.Count

$y.Length is 3,4 which seems to correspond to the synthesized .Length for each of the strings. $y.Count, on the other hand, is 2. I _think_ that ('1','2').Length is 2 (from my prior example) because [array] has a .Length property for some reason.

Ah, right. If the object is enumerable, then we enumerate. Array adds an alias so it has both Count and Length, but other enumerable types do not, so it's not entirely consistent.

In hindsight, maybe we should have only added Count and not Length to every object.

Was this page helpful?
0 / 5 - 0 ratings