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.
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 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
## 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
> $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
```
@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.
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.
Most helpful comment
I agree. We need to decide which behavior is the correct one.
From my personal view,
Where-Objectseems doing the right thing because just like @BrucePay said, a script block is an anonymous function.Here is another example:
Here,
b.ps1contains an invocation of a script block, and as we can see, the script block doesn't usesb.ps1as the command name.10454 is related to this issue. The PR made an optimization to override
ForEahc-Objectwith dot-sourcing a filter-like script block for some common uses ofForEach-Object, and when that optimization kicks in,$MyInvocation.MyCommand.NameisnullforForEach-Objecttoo.Whether that should be fixed and how to fix it depends on the conclusion of this issue.
Here are the output of
$MyInvocationfromWhere-ObjectandForEach-Objectin the repro scenario as inpreview.3:Here is the output of
$MyInvocationfromForEach-Objectin the repro scenario with the optimization change in #10454: