[英]Supporting lexical-scope ScriptBlock parameters (e.g. Where-Object)
Consider the following arbitrary function and test cases: 考虑以下任意函数和测试用例:
Function Foo-MyBar {
Param(
[Parameter(Mandatory=$false)]
[ScriptBlock] $Filter
)
if (!$Filter) {
$Filter = { $true }
}
#$Filter = $Filter.GetNewClosure()
Get-ChildItem "$env:SYSTEMROOT" | Where-Object $Filter
}
##################################
$private:pattern = 'T*'
Get-Help Foo-MyBar -Detailed
Write-Host "`n`nUnfiltered..."
Foo-MyBar
Write-Host "`n`nTest 1:. Piped through Where-Object..."
Foo-MyBar | Where-Object { $_.Name -ilike $private:pattern }
Write-Host "`n`nTest 2:. Supplied a naiive -Filter parameter"
Foo-MyBar -Filter { $_.Name -ilike $private:pattern }
In Test 1, we pipe the results of Foo-MyBar
through a Where-Object
filter, which compares the objects returned to a pattern contained in a private-scoped variable $private:pattern
. 在测试1中,我们通过
Where-Object
过滤器管道Foo-MyBar
的结果,该过滤器将返回的对象与包含在私有范围变量$private:pattern
。 In this case, this correctly returns all the files/folders in C:\\ which start with the letter T
. 在这种情况下,这将正确返回C:\\中以字母
T
开头的所有文件/文件夹。
In Test 2, we pass the same filtering script directly as a parameter to Foo-MyBar
. 在测试2中,我们将相同的过滤脚本作为参数直接传递给
Foo-MyBar
。 However, by the time Foo-MyBar
gets to running the filter, $private:pattern
is not in scope, and so this returns no items. 但是,当
Foo-MyBar
运行过滤器时, $private:pattern
不在范围内,因此不返回任何项目。
I understand why this is the case -- because the ScriptBlock passed to Foo-MyBar
is not a closure , so does not close over the $private:pattern
variable and that variable is lost. 我明白为什么会这样 - 因为传递给
Foo-MyBar
不是一个闭包 ,所以不会关闭$private:pattern
变量而该变量会丢失。
I note from comments that I previously had a flawed third test, which tried to pass {...}.GetNewClosure(), but this does not close over private-scoped variables -- thanks @PetSerAl for helping me clarify that. 我从评论中注意到我之前有一个有缺陷的第三个测试,它试图传递{...}。GetNewClosure(),但这并没有关闭私有范围的变量 - 感谢@PetSerAl帮助我澄清这一点。
The question is, how does Where-Object
capture the value of $private:pattern
in Test 1, and how do we achieve the same behaviour in our own functions/cmdlets? 问题是,
Where-Object
如何捕获测试1中$private:pattern
的值,以及我们如何在我们自己的函数/ cmdlet中实现相同的行为?
(Preferably without requiring the caller to have to know about closures, or know to pass their filter script as a closure.) (最好不要求调用者必须知道闭包,或者知道将过滤器脚本作为闭包传递。)
I note that, if I uncomment the $Filter = $Filter.GetNewClosure()
line inside Foo-MyBar
, then it never returns any results, because $private:pattern
is lost. 我注意到,如果我取消注释
Foo-MyBar
的$Filter = $Filter.GetNewClosure()
行,那么它永远不会返回任何结果,因为$private:pattern
会丢失。
(As I said at the top, the function and parameter are arbitrary here, as a shortest-form reproduction of my real problem!) (正如我在顶部所说,函数和参数在这里是任意的,作为我真实问题的最短形式再现!)
how does
Where-Object
capture the value of$private:pattern
in Test 1Where-Object
如何在测试1中捕获$private:pattern
的值
As can be seen in the source code for Where-Object
in PowerShell Core , PowerShell internally invokes the filter script without confining it to its own local scope ( _script
is the private backing field for the FilterScript
parameter, notice the useLocalScope: false
argument passed to DoInvokeReturnAsIs()
): 从PowerShell Core中
Where-Object
的源代码中可以看出,PowerShell在内部调用过滤器脚本而不将其限制在自己的本地范围内 ( _script
是FilterScript
参数的私有后备字段,请注意传递给useLocalScope: false
参数DoInvokeReturnAsIs()
):
protected override void ProcessRecord()
{
if (_inputObject == AutomationNull.Value)
return;
if (_script != null)
{
object result = _script.DoInvokeReturnAsIs(
useLocalScope: false, // <-- notice this named argument right here
errorHandlingBehavior: ScriptBlock.ErrorHandlingBehavior.WriteToCurrentErrorPipe,
dollarUnder: InputObject,
input: new object[] { _inputObject },
scriptThis: AutomationNull.Value,
args: Utils.EmptyArray<object>());
if (_toBoolSite.Target.Invoke(_toBoolSite, result))
{
WriteObject(InputObject);
}
}
// ...
}
how do we achieve the same behaviour in our own functions/cmdlets?
我们如何在自己的函数/ cmdlet中实现相同的行为?
We don't - DoInvokeReturnAsIs()
(and similar scriptblock invocation facilities) are marked internal
and can therefore only be invoked by types contained in the System.Management.Automation
assembly 我们没有 -
DoInvokeReturnAsIs()
(和类似的scriptblock调用工具)被标记为internal
,因此只能由System.Management.Automation
程序集中包含的类型调用
The example given does not work because calling a function will enter a new scope by default. 给出的示例不起作用,因为默认情况下调用函数将进入新范围。
Where-Object
will still invoke the filter script without entering one, but the scope of the function does not have the private
variable. Where-Object
仍将调用过滤器脚本而不输入过滤器脚本,但该函数的范围没有private
变量。
There's three ways around this. 有三种方法可以解决这个问题。
Every module has a SessionState
which has its own stack of SessionStateScope
s. 每个模块都有一个
SessionState
,它有自己的SessionStateScope
堆栈。 Every ScriptBlock
is tied to the SessionState
is was parsed in. 每个
ScriptBlock
都与解析的SessionState
相关联。
If you call a function defined in a module, a new scope is created within that module's SessionState
, but not within the top level SessionState
. 如果调用模块中定义的函数,则会在该模块的
SessionState
创建新范围,但不会在顶级SessionState
。 Therefore when Where-Object
invokes the filter script without entering a new scope, it does so on the current scope for the SessionState
to which that ScriptBlock
is tied. 因此,当
Where-Object
在不输入新范围的情况ScriptBlock
滤器脚本时,它会在与该ScriptBlock
绑定的SessionState
的当前范围上执行此操作。
This is a bit fragile, because if you want to call that function from your module, well you can't. 这有点脆弱,因为如果你想从你的模块中调用该函数,那么你就不能。 It'll have the same issue.
它会有同样的问题。
You most likely already know the dot-source operator ( .
) for invoking script files without creating a new scope. 您很可能已经知道用于在不创建新范围的情况下调用脚本文件的点源运算符(
.
)。 That also works with command names and ScriptBlock
objects. 这也适用于命令名称和
ScriptBlock
对象。
. { 'same scope' }
. Foo-MyBar
Note, however, that this will invoke the function within the current scope of the SessionState
that the function is from , so you cannot rely on .
但是请注意,这将调用函数来自的
SessionState
的当前范围内的函数 ,因此您不能依赖.
to always execute in the caller's current scope. 总是在调用者的当前范围内执行。 Therefore, if you invoke functions associated with a different
SessionState
with the dot-source operator - such as functions defined in a (different) module - it may have unintended effects. 因此,如果使用点源运算符调用与不同
SessionState
关联的函数(例如(不同)模块中定义的函数),则可能会产生意外影响。 Variables created will persist to future function invocations and any helper functions defined within the function itself will also persist. 创建的变量将持续存在于将来的函数调用中,并且函数本身中定义的任何辅助函数也将持续存在。
Compiled commands (cmdlets) do not create a new scope when invoked. 编译的命令(cmdlet)在调用时不会创建新的作用域。 You can also use similar API's to what
Where-Object
use (though not the exact same ones) 您也可以使用类似的API来使用
Where-Object
(尽管不是完全相同的)
Here's a rough implementation of how you could implement Where-Object
using public API's 下面是如何使用公共API实现
Where-Object
的粗略实现
using System.Management.Automation;
namespace MyModule
{
[Cmdlet(VerbsLifecycle.Invoke, "FooMyBar")]
public class InvokeFooMyBarCommand : PSCmdlet
{
[Parameter(ValueFromPipeline = true)]
public PSObject InputObject { get; set; }
[Parameter(Position = 0)]
public ScriptBlock FilterScript { get; set; }
protected override void ProcessRecord()
{
var filterResult = InvokeCommand.InvokeScript(
useLocalScope: false,
scriptBlock: FilterScript,
input: null,
args: new[] { InputObject });
if (LanguagePrimitives.IsTrue(filterResult))
{
WriteObject(filterResult, enumerateCollection: true);
}
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.