Powershell: $MyInvocation.MyCommand.Name becomes null in Where-Object block

Created on 3 Sep 2019  路  16Comments  路  Source: PowerShell/PowerShell


Issue description:
Unable to use $MyInvocation.MyCommand.Name in Where-Object block as it becomes null. ForEach and For-EachObject works fine. I see this only with Where-Object.

I understand there are workarounds but just want to know if this is expected behavior or not as I was not able to find document that describes scope in Where-Object.

Steps to reproduce

Copy the following script to test.ps1

1 | Where-Object{
    Write-Host('MyInvocation in Where-Object: ' + $MyInvocation.MyCommand.Name)
}

1 | ForEach-Object{
    Write-Host('MyInvocation in ForEach-Object: ' + $MyInvocation.MyCommand.Name)
}

ForEach($FileName in 1){
    Write-Host('MyInvocation in ForEach: ' + $MyInvocation.MyCommand.Name)
}

Then run

.\test.ps1

Expected behavior

## Expected output in case script name is test.ps1.

MyInvocation in Where-Object: test.ps1
MyInvocation in ForEach-Object: test.ps1
MyInvocation in ForEach: test.ps1

Actual behavior

## Expected output in case script name is test.ps1.

MyInvocation in Where-Object:       <-------- $MyInvocation.MyCommand.Name becomes null 
MyInvocation in ForEach-Object: test.ps1
MyInvocation in ForEach: test.ps1


Environment data

> $PSVersionTable

Name Value
---- -----
PSVersion 6.2.1
PSEdition Core
GitCommitId 6.2.1
OS Microsoft Windows 10.0.18362
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0

```

Committee-Reviewed Issue-Question Up-for-Grabs

Most helpful comment

I agree. We need to decide which behavior is the correct one.
From my personal view, Where-Object seems doing the right thing because just like @BrucePay said, a script block is an anonymous function.

Here is another example:

PS> cat .\b.ps1
. { Write-Host('MyInvocation in the script block: ' + $MyInvocation.MyCommand.Name) }
PS> .\b.ps1
MyInvocation in the script block:

Here, b.ps1 contains an invocation of a script block, and as we can see, the script block doesn't uses b.ps1 as the command name.

10454 is related to this issue. The PR made an optimization to override ForEahc-Object with dot-sourcing a filter-like script block for some common uses of ForEach-Object, and when that optimization kicks in, $MyInvocation.MyCommand.Name is null for ForEach-Object too.

Whether that should be fixed and how to fix it depends on the conclusion of this issue.


Here are the output of $MyInvocation from Where-Object and ForEach-Object in the repro scenario as in preview.3:

MyInvocation in ForEach-Object:

MyCommand             : a.ps1
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 1
OffsetInLine          : 1
HistoryId             : 46
ScriptName            :
Line                  : .\a.ps1
PositionMessage       : At line:1 char:1
                        + .\a.ps1
                        + ~~~~~~~
PSScriptRoot          :
PSCommandPath         :
InvocationName        : .\a.ps1
PipelineLength        : 1
PipelinePosition      : 1
ExpectingInput        : False
CommandOrigin         : Runspace
DisplayScriptPosition :
MyInvocation in Where-Object:

MyCommand             :
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 1
OffsetInLine          : 1
HistoryId             : 47
ScriptName            :
Line                  : .\c.ps1
PositionMessage       : At line:1 char:1
                        + .\c.ps1
                        + ~~~~~~~
PSScriptRoot          :
PSCommandPath         :
InvocationName        :
PipelineLength        : 0
PipelinePosition      : 0
ExpectingInput        : False
CommandOrigin         : Internal
DisplayScriptPosition :

Here is the output of $MyInvocation from ForEach-Object in the repro scenario with the optimization change in #10454:

MyInvocation in ForEach-Object:

MyCommand             :
                            'MyInvocation in ForEach-Object: '
                            $MyInvocation | fl *

BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 36
HistoryId             : 32
ScriptName            : F:\tmp\a.ps1
Line                  : 1..2 | Where-Object { $_ -gt 1 } | ForEach-Object {

PositionMessage       : At F:\tmp\a.ps1:5 char:36
                        + 1..2 | Where-Object { $_ -gt 1 } | ForEach-Object {
                        +                                    ~~~~~~~~~~~~~~~~
PSScriptRoot          : F:\tmp
PSCommandPath         : F:\tmp\a.ps1
InvocationName        : ForEach-Object
PipelineLength        : 2
PipelinePosition      : 2
ExpectingInput        : True
CommandOrigin         : Internal
DisplayScriptPosition :

All 16 comments

@ryhayash

I understand there are workarounds but just want to know if this is expected behavior

This behaviour is the logical consequence of what you're doing. Unlike the foreach statement which just has a block as part of the statement., Where-Object is a _command_ that takes a script block _object_ as it's argument. A scriptblock is an anonymous function (anonymous command) so it has it's own invocation information and, since it's anonymous, the command name is empty. The other fields are populated however e.g.

{master}PSCore (1:11) >  & { $MyInvocation.MyCommand.CommandType}
Script

@BrucePay if that's the case, why does the behaviour differ between Where-Object and ForEach-Object? They are both cmdlets, and I don't think it's unreasonable for users to expect a consistent experience between these two commands in terms of how their scriptblock implementations behave.

I agree. We need to decide which behavior is the correct one.
From my personal view, Where-Object seems doing the right thing because just like @BrucePay said, a script block is an anonymous function.

Here is another example:

PS> cat .\b.ps1
. { Write-Host('MyInvocation in the script block: ' + $MyInvocation.MyCommand.Name) }
PS> .\b.ps1
MyInvocation in the script block:

Here, b.ps1 contains an invocation of a script block, and as we can see, the script block doesn't uses b.ps1 as the command name.

10454 is related to this issue. The PR made an optimization to override ForEahc-Object with dot-sourcing a filter-like script block for some common uses of ForEach-Object, and when that optimization kicks in, $MyInvocation.MyCommand.Name is null for ForEach-Object too.

Whether that should be fixed and how to fix it depends on the conclusion of this issue.


Here are the output of $MyInvocation from Where-Object and ForEach-Object in the repro scenario as in preview.3:

MyInvocation in ForEach-Object:

MyCommand             : a.ps1
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 1
OffsetInLine          : 1
HistoryId             : 46
ScriptName            :
Line                  : .\a.ps1
PositionMessage       : At line:1 char:1
                        + .\a.ps1
                        + ~~~~~~~
PSScriptRoot          :
PSCommandPath         :
InvocationName        : .\a.ps1
PipelineLength        : 1
PipelinePosition      : 1
ExpectingInput        : False
CommandOrigin         : Runspace
DisplayScriptPosition :
MyInvocation in Where-Object:

MyCommand             :
BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 1
OffsetInLine          : 1
HistoryId             : 47
ScriptName            :
Line                  : .\c.ps1
PositionMessage       : At line:1 char:1
                        + .\c.ps1
                        + ~~~~~~~
PSScriptRoot          :
PSCommandPath         :
InvocationName        :
PipelineLength        : 0
PipelinePosition      : 0
ExpectingInput        : False
CommandOrigin         : Internal
DisplayScriptPosition :

Here is the output of $MyInvocation from ForEach-Object in the repro scenario with the optimization change in #10454:

MyInvocation in ForEach-Object:

MyCommand             :
                            'MyInvocation in ForEach-Object: '
                            $MyInvocation | fl *

BoundParameters       : {}
UnboundArguments      : {}
ScriptLineNumber      : 5
OffsetInLine          : 36
HistoryId             : 32
ScriptName            : F:\tmp\a.ps1
Line                  : 1..2 | Where-Object { $_ -gt 1 } | ForEach-Object {

PositionMessage       : At F:\tmp\a.ps1:5 char:36
                        + 1..2 | Where-Object { $_ -gt 1 } | ForEach-Object {
                        +                                    ~~~~~~~~~~~~~~~~
PSScriptRoot          : F:\tmp
PSCommandPath         : F:\tmp\a.ps1
InvocationName        : ForEach-Object
PipelineLength        : 2
PipelinePosition      : 2
ExpectingInput        : True
CommandOrigin         : Internal
DisplayScriptPosition :

Don't forget about ForEach -Parallel - we need to have a consistency with it too.

At the meantime, I also doubt if it's necessary to pursue the consistency of $MyInvocation for ForEach-Object and Where-Object. I will analyze the powershell corpus to find out if there is any uses of $MyInvocation within the Where/ForEach-Object script block arguments.

I analyzed the powershell corpus.ForEach-Object appears 260954 times in those scripts, and the variable $MyInvocation is used in 294 ForEach-Object invocations. Out of that 294 uses, 259 are $MyInvocation.MyCommand.XXX.
Given this analysis result, the pipeline-rewriting optimization is a practical breaking change, and has been reverted.

Is there no way to implement the optimization without the breaking change? 馃檨

I looked into how feasible to fix this with the pipeline-rewriting. It turned out not possible unless with some hacky code to break how the ScriptCommandProcessor works today, for example, making the InternalCommand.MyInvocation settable, which might introduce other problems because the rest of code assumes InternalCommand.MyInvocation reflects exactly the InternalCommand.

Does the .MyInvocation work right in filter function?

@iSazonov I'm not clear what you are asking. For reference, below are the places where the InvocationInfo is constructed for ForEach-Object and Where-Object. Basically, it's impossible to retain the exact same InvocationInfo if we replace the ForEach-Objcet with a script command.

ForEach-Object:
https://github.com/PowerShell/PowerShell/blob/cc0fed479a3e455b746a3d12597f078462f2d644/src/System.Management.Automation/engine/lang/scriptblock.cs#L676-L677

Where-Object:
https://github.com/PowerShell/PowerShell/blob/cc0fed479a3e455b746a3d12597f078462f2d644/src/System.Management.Automation/engine/runtime/CompiledScriptBlock.cs#L1051-L1059

Basically, it's impossible to retain the exact same InvocationInfo if we replace the ForEach-Objcet with a script command.

I see. My comment was that we consider ForEach-Object, Where-Object and ForEach (and .ForEach()) but there is filter functions. I guess that ForEach and filter functions have the same behaviour.

We have or could have other cmdlets with scriptblock parameters. What is their behavior?

I guess that ForEach and filter functions have the same behaviour.

No, Foreach-Object and filter functions (or script block with the process block only) have different behavior when it comes to $MyInvocation.

We have or could have other cmdlets with scriptblock parameters. What is their behavior?

That depends on which XXInvokeXX method is being used for those script block arguments.

Should we be matching which XXInvokeXX method is being used between ForEach-Object and Where-Object?

I don't know. It sounds like a breaking change. And again, the question would be back to is it necessary to pursue the consistency in these two particular cases.

That depends on which XXInvokeXX method is being used for those script block arguments.

If we allow this for third-party cmdlets why do we search a consistency for core cmdlets?

If a conclusion will be that we should use only one way this will mean that we should deprecate a public API, right?

@PowerShell/powershell-committee reviewed this. Inspecting the code, it appears that both Where-Object and ForEach-Object both have code to set MyInvocation, but using different APIs. It seems that user found utility in getting this information so we recommend that Where-Object should behave like ForEach-Object even if it is technically correct that it's an anonymous function.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

concentrateddon picture concentrateddon  路  3Comments

SteveL-MSFT picture SteveL-MSFT  路  3Comments

garegin16 picture garegin16  路  3Comments

JohnLBevan picture JohnLBevan  路  3Comments

alx9r picture alx9r  路  3Comments