PowerShell / PowerShell

PowerShell for every system!
https://microsoft.com/PowerShell
MIT License
44.82k stars 7.25k forks source link

$MyInvocation.MyCommand.Name becomes null in Where-Object block #10477

Closed ryhayash closed 10 months ago

ryhayash commented 5 years ago

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
BrucePay commented 5 years ago

@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
vexx32 commented 5 years ago

@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.

daxian-dbw commented 5 years ago

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 :
iSazonov commented 5 years ago

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

daxian-dbw commented 5 years ago

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.

daxian-dbw commented 5 years ago

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.

vexx32 commented 5 years ago

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

daxian-dbw commented 5 years ago

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.

iSazonov commented 5 years ago

Does the .MyInvocation work right in filter function?

daxian-dbw commented 5 years ago

@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

iSazonov commented 5 years ago

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?

daxian-dbw commented 5 years ago

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.

vexx32 commented 5 years ago

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

daxian-dbw commented 5 years ago

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.

iSazonov commented 5 years ago

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?

SteveL-MSFT commented 4 years ago

@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.

microsoft-github-policy-service[bot] commented 11 months ago

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

microsoft-github-policy-service[bot] commented 11 months ago

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

microsoft-github-policy-service[bot] commented 11 months ago

This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.

microsoft-github-policy-service[bot] commented 10 months ago

This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.