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-Object
seems doing the right thing because just like @BrucePay said, a script block is an anonymous function.Here is another example:
Here,
b.ps1
contains an invocation of a script block, and as we can see, the script block doesn't usesb.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 ofForEach-Object
, and when that optimization kicks in,$MyInvocation.MyCommand.Name
isnull
forForEach-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
fromWhere-Object
andForEach-Object
in the repro scenario as inpreview.3
:Here is the output of
$MyInvocation
fromForEach-Object
in the repro scenario with the optimization change in #10454: