Powershell: Add-Member's inconsistent pipeline input object behavior

Created on 6 Jun 2019  路  8Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

$Info = @(
    'a',
    'b',
    'c'
)

0..3 | Add-Member -NotePropertyMembers @{'Info' = $Info[$_]} -PassThru | Select Info

Expected behavior

$Info = @(
    'a',
    'b',
    'c'
)

0..3 | % { Add-Member -InputObject $_ -NotePropertyMembers @{'Info' = $Info[$_]} -PassThru } | Select Info

```none

Info

a
b
c


Or, to illustrate the inconsistency, seemingly caused when you try to access `$_`:

```powershell
$Info = @(
    'a',
    'b',
    'c'
)

0..3 | Add-Member -NotePropertyMembers @{'Info' = 'Info'} -PassThru | Select Info

```none

Info

Info
Info
Info


# Actual behavior

```none
Index operation failed; the array index evaluated to null.
At line:9 char:1
+ 0..3 | Add-Member -NotePropertyMembers @{'Info' = $Info[$_]} -PassThr ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArrayIndex

Environment data

Name                           Value
----                           -----
PSVersion                      6.2.0
PSEdition                      Core
GitCommitId                    6.2.0
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Issue-Question Resolution-Answered

All 8 comments

Hi @burkasaurusrex, as near as i can tell, you're getting the error because a) you have set-strictmode -version latest turned on and b) you're passing 4 objects into the pipeline but only have 3 objects in the $info array. This means that the last object in the pipeline will cause an out-of-bounds error. Now if strictmode was off, then the last note added would be bound to null rather than producing an error. Does this sound right or am I missing something?

Thanks Bruce. I'm not running in strict mode and I'm still showing the same error if you only pass three objects:

$Info = @(
    'a',
    'b',
    'c'
)

0..2 | Add-Member -NotePropertyMembers @{'Info' = $Info[$_]} -PassThru | Select Info
Index operation failed; the array index evaluated to null.
At line:7 char:1
+ 0..2 | Add-Member -NotePropertyMembers @{'Info' = $Info[$_]} -PassThr ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : NullArrayIndex

Are you not getting the same error? This feels like a bug where the pipeline object isn't getting picked up by Add-Member, so you have to explicitly set -InputObject to access $_

Not a bug. When you pipe something directly into a cmdlet, you don't have access to the $_ variable. It doesn't exist in the bare pipeline context. It exists in ForEach-Object, and it _can_ exist for parameters defined as being pipeline-bindable either by value or by property name.

If that were the case with -NotePropertyMembers (it isn't, I just checked the help page, that parameter doesn't work with pipeline input) you could do what you're trying to do by adding a script block around the value expression here:

0..2 | Add-Member -NotePropertyMembers { @{'Info' = $Info[$_]} } -PassThru | Select Info

However, this parameter was not defined in this way, and likely also wasn't coded in the necessary fashion under the hood to enable this.

This absolutely could be done, I think, I don't immediately see a reason to avoid doing this, but it'd be an enhancement request rather than a bug fix.

So yeah, the issue is akin to doing, for example $a | Get-ChildItem -Filter $_

That variable isn't bound in the bare pipeline, only in scriptblock binding to pipeline parameters and the scriptblocks provided by ForEach-Object and the like.

Appreciate the insight. Still seems strange to me that this works no problem:

$Info = @(
    'a',
    'b',
    'c'
)

0..2 | % { Add-Member -InputObject $_ -NotePropertyMembers @{'Info' = $Info[$_]} -PassThru } | Select Info

So if you exclude -InputObject $_ it errors out, but if you include it, it works. So even though -NotePropertyMembers doesn't have pipeline access, it can still access the -InputObject using $_? And if it can access the -InputObject using $_, isn't the normal behavior of -InputObject in other cmdlets to pick up the object from the pipeline?

This works because of the extra cmdlet you added. % isn't a language operation, it's an alias for the ForEach-Object cmdlet; this cmdlet is what's allocating value to $_ within the scriptblock(s) you pass to it. 馃檪

You are correct that -InputObject "picks up" pipeline input, but that doesn't automatically happen unless you're piping directly into that cmdlet. You're not piping into Add-Member there, you're piping into ForEach-Object, which then assigns the input object to $_ so that Add-Member can utilise it.

It's a bit easier to demonstrate if we write it without aliases:

$Info = @(
    'a',
    'b',
    'c'
)

0..2 | ForEach-Object {
    Add-Member -InputObject $_ -NotePropertyMembers @{ 'Info' = $Info[$_] } -PassThru
} | Select-Object -Property Info

Yea, that part makes sense to me. I appreciate the back-and-forth - it's helped clarify why Add-Member's pipeline behavior feels inconsistent.

I probably should have called out a different example. Your example above works no problem, but this does not (removing -InputObject $_):

$Info = @(
    'a',
    'b',
    'c'
)

0..2 | ForEach-Object {
    Add-Member -NotePropertyMembers @{ 'Info' = $Info[$_] } -PassThru
} | Select-Object -Property Info

Instead it prompts for an InputObject:

cmdlet Add-Member at command pipeline position 1
Supply values for the following parameters:
InputObject:

Looking at the parameters in different help files, it looks like Add-Member's -InputObject is mandatory while other more pipeline-friendly cmdlets (Select-Object, Measure-Object, etc.) are optional (though all of them mention that there is no default value...). I haven't dug into the code yet, but I'm guessing -InputObject for Select-Object and others defaults to the pipeline object?

Given that Add-Member is used to manipulate objects like these other cmdlets, it would make sense to me to have similar pipeline treatment. I'm hoping that makes more sense? Thanks again for the patience.

No problem! ^^

The scriptblock of a ForEach-Object command doesn't automatically pass the inputobject to all cmdlets executed within it. This would be a rather confusing if it did; imagine this case, for example:

1..10 | ForEach-Object {
    Get-ChildItem
    Add-Member -NotePropertyMembers @{ 'Info' = $Info[$_] } -PassThru
}

Which command(s) should receive the complete pipeline input object? It's not clear what that would do if it behaved as you are expecting. This isn't Add-Member specific; all pipeline-capable cmdlets behave this way.

You can enter any number of commands in a ForEach-Object statement, it's just a regular old scriptblock like any other. Cmdlets within are not passed _any_ input from the pipeline without explicitly referencing the $_ automatic variable (you'll also see it referred to as $PSItem; these are equivalent).

Where pipeline input is concerned, it's a pretty one-or-the-other; if you're _directly_ piping input into a cmdlet, you don't typically use $_. If you're putting the command(s) in a ForEach-Object cmdlet's scriptblock, you _must_ use $_ to reference the input being passed in. This is because the ForEach-Object cmdlet itself is actually consuming the pipeline input and then assigning it to the $_ variable inside its scriptblocks.

This issue has been marked as answered and has not had any activity for 1 day. It has been closed for housekeeping purposes.

Was this page helpful?
0 / 5 - 0 ratings