本文介绍了支持词法范围的ScriptBlock参数(例如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 }

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

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. In this case, this correctly returns all the files/folders in C:\ which start with the letter T.

在测试2中,我们将相同的过滤脚本直接作为参数传递给 Foo-MyBar 。但是,当 Foo-MyBar 开始运行过滤器时, $ private:pattern 不在范围内,并且

In Test 2, we pass the same filtering script directly as a parameter to 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.

我理解为什么是这种情况-因为ScriptBlock传递给 Foo- MyBar 不是 closure ,因此不会关闭 $ 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.

我从评论中注意到,我以前有一个有缺陷的第三次测试,该测试试图通过{...}。GetNewClosure(),但此操作无法关闭关于私有范围的变量-感谢@PetSerAl帮我澄清了这个问题。

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

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?

(最好不要求调用者必须了解闭包,也不必知道将其过滤脚本作为闭包传递。)

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

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.

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

(As I said at the top, the function and parameter are arbitrary here, as a shortest-form reproduction of my real problem!)

推荐答案

给出的示例不起作用,因为默认情况下调用函数将输入新的作用域。 Where-Object 仍将调用过滤器脚本而不输入任何脚本,但是该函数的范围没有 private

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.

有三种解决方法。

每个模块都有一个 SessionState ,它具有自己的 SessionStateScope s。每个 ScriptBlock 都与 SessionState 相关联。

Every module has a SessionState which has its own stack of SessionStateScopes. Every ScriptBlock is tied to the SessionState is was parsed in.

如果调用模块中定义的函数,则会在该模块的 SessionState 中创建一个新作用域,但不在顶级 SessionState 。因此,当 Where-Object 调用过滤器脚本而不输入新的作用域时,它将在 SessionState ScriptBlock 绑定到的>。

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. 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.

这有点脆弱,因为如果要调用该函数从您的模块,那么您不能。会遇到同样的问题。

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.

您很可能已经知道点源运算符()用于调用脚本文件而不创建新作用域。这也适用于命令名称和 ScriptBlock 对象。

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.

. { 'same scope' }
. Foo-MyBar

但是请注意,这将在<$的当前范围内调用该函数c $ c> SessionState 该函数来自,因此您不能依赖始终在呼叫者的当前范围。因此,如果您使用点源运算符调用与其他 SessionState 相关的函数,例如(不同的)模块中定义的函数,则可能会产生意想不到的效果。创建的变量将保留到以后的函数调用中,并且在函数本身中定义的任何辅助函数也将保留。

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 . 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. Variables created will persist to future function invocations and any helper functions defined within the function itself will also persist.

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

Compiled commands (cmdlets) do not create a new scope when invoked. 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

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);
            }
        }
    }
}

这篇关于支持词法范围的ScriptBlock参数(例如Where-Object)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-07 04:36