简体   繁体   English

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

[英]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 1 Where-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在内部调用过滤器脚本而不将其限制在自己的本地范围内_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);
        }
    }
    // ...
}

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. 有三种方法可以解决这个问题。

Put the function in a different module than the caller 将功能放在与呼叫者不同的模块中

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. 它会有同样的问题。

Call the function with the dot source operator 使用点源运算符调用该函数

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. 创建的变量将持续存在于将来的函数调用中,并且函数本身中定义的任何辅助函数也将持续存在。

Write a Cmdlet 写一个Cmdlet

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.

相关问题 powershell 在脚本 function 中使用 where-object 不起作用 - powershell use of where-object within script function not working 将powershell变量放入where对象不返回数据 - powershell variable into where-object does not return data 为什么在对象本身(例如Function.name)上定义Function属性,而在Function.prototype上定义方法? - Why do Function properties are defined on the object itself (e.g. Function.name), but methods - on Function.prototype? Python:如何创建一个函数? 例如 f(x) = ax^2 - Python: How to create a function? e.g. f(x) = ax^2 如何调用一个函数来重新加载另一个函数(例如在“ orientationchange()”上)? - How to call a function to reload another function (on “orientationchange()” e.g.)? 大括号中的数字例如是什么? “{0}”是什么意思? - What do numbers in braces e.g. “{0}” mean? Oracle SQL:例如在4个小时内计数和求和? - Oracle SQL: Count and sum in e.g. 4 hours? DBMS_SQL.BIND_VAR转换为函数(例如sysdate) - DBMS_SQL.BIND_VAR to a function (e.g. sysdate) C:字符串,例如“abcde123”到int 123 - C: String e.g. “abcde123” to int 123 带 Word 的数组值,例如二 - 2、三 - 3 - Array values with Word for e.g. Two - 2, Three - 3
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM