支持词法范围的ScriptBlock参数(例如Where-Object)

jim*_*gee 6 powershell closures scope function scriptblock

考虑以下任意函数和测试用例:

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 }
Run Code Online (Sandbox Code Playgroud)

在测试1中,我们Foo-MyBar通过Where-Object过滤器管道结果,该过滤器将返回的对象与包含在私有范围变量中的模式进行比较$private:pattern.在这种情况下,这将正确返回C:\中以字母开头的所有文件/文件夹T.

在测试2中,我们直接将相同的过滤脚本作为参数传递给Foo-MyBar.但是,Foo-MyBar到运行过滤器时,$private:pattern不在范围内,因此不返回任何项目.

我理解为什么会这样 - 因为传递给ScriptBlock的Foo-MyBar不是闭包,因此不会关闭$private:pattern变量并且该变量会丢失.

我从评论中注意到我之前有一个有缺陷的第三个测试,它试图通过{...}.GetNewClosure(),但这并没有关闭私有范围的变量 - 感谢@PetSerAl帮助我澄清这一点.

问题是,如何Where-Object捕获$private:pattern测试1中的值,以及我们如何在我们自己的函数/ cmdlet中实现相同的行为?

(最好不要求调用者必须知道闭包,或者知道将过滤器脚本作为闭包传递.)

我注意到,如果我取消注释$Filter = $Filter.GetNewClosure()内部的行Foo-MyBar,那么它永远不会返回任何结果,因为$private:pattern它会丢失.

(正如我在顶部所说,函数和参数在这里是任意的,作为我真实问题的最短形式再现!)

Mat*_*sen 6

如何Where-Object捕获$private:pattern测试1中的值

PowerShell Core 的源代码中Where-Object可以看出,PowerShell在内部调用过滤器脚本而不将其限制在自己的本地范围内(_scriptFilterScript参数的私有支持字段,注意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);
        }
    }
    // ...
}
Run Code Online (Sandbox Code Playgroud)

我们如何在自己的函数/ cmdlet中实现相同的行为?

我们没有 - DoInvokeReturnAsIs()(和类似的scriptblock调用工具)被标记internal,因此只能由System.Management.Automation程序集中包含的类型调用


Pat*_*cke 5

给出的示例不起作用,因为默认情况下调用函数将进入新范围. Where-Object仍然会在不输入过滤器脚本的情况下调用过滤器脚本,但该函数的范围没有private变量.

有三种方法可以解决这个问题.

将功能放在与呼叫者不同的模块中

每个模块都有一个SessionState拥有自己的SessionStateScopes 堆栈.每个ScriptBlockSessionState被解析为被解析.

如果调用模块中定义的函数,则会在该模块中创建新范围SessionState,但不会在顶级范围内创建SessionState.因此,在Where-Object不输入新范围的情况下调用过滤器脚本时,它会在绑定SessionState到的范围的当前范围上执行此操作ScriptBlock.

这有点脆弱,因为如果你想从你的模块中调用该函数,那么你就不能.它会有同样的问题.

使用点源运算符调用该函数

您很可能已经知道.用于在不创建新范围的情况下调用脚本文件的点源运算符().这也适用于命令名称和ScriptBlock对象.

. { 'same scope' }
. Foo-MyBar
Run Code Online (Sandbox Code Playgroud)

但请注意,这将调用函数所在的当前作用域内SessionState 的函数,因此您不能依赖于.始终在调用者的当前作用域中执行.因此,如果SessionState使用点源运算符调用与其他函数关联的函数(例如(不同)模块中定义的函数),则可能会产生意外影响.创建的变量将持续存在于将来的函数调用中,并且函数本身中定义的任何辅助函数也将持续存在.

写一个Cmdlet

编译的命令(cmdlet)在调用时不会创建新的作用域.您也可以使用类似的API来Where-Object使用(尽管不是完全相同的)

以下是如何Where-Object使用公共API 实现的粗略实现

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);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)