簡體   English   中英

支持詞法范圍的ScriptBlock參數(例如Where-Object)

[英]Supporting lexical-scope ScriptBlock parameters (e.g. Where-Object)

考慮以下任意函數和測試用例:

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中,我們通過Where-Object過濾器管道Foo-MyBar的結果,該過濾器將返回的對象與包含在私有范圍變量$private:pattern 在這種情況下,這將正確返回C:\\中以字母T開頭的所有文件/文件夾。

在測試2中,我們將相同的過濾腳本作為參數直接傳遞給Foo-MyBar 但是,當Foo-MyBar運行過濾器時, $private:pattern不在范圍內,因此不返回任何項目。

我明白為什么會這樣 - 因為傳遞給Foo-MyBar不是一個閉包 ,所以不會關閉$private:pattern變量而該變量會丟失。

我從評論中注意到我之前有一個有缺陷的第三個測試,它試圖傳遞{...}。GetNewClosure(),但這並沒有關閉私有范圍的變量 - 感謝@PetSerAl幫助我澄清這一點。

問題是, Where-Object如何捕獲測試1中$private:pattern的值,以及我們如何在我們自己的函數/ cmdlet中實現相同的行為?

(最好不要求調用者必須知道閉包,或者知道將過濾器腳本作為閉包傳遞。)

我注意到,如果我取消注釋Foo-MyBar$Filter = $Filter.GetNewClosure()行,那么它永遠不會返回任何結果,因為$private:pattern會丟失。

(正如我在頂部所說,函數和參數在這里是任意的,作為我真實問題的最短形式再現!)

Where-Object如何在測試1中捕獲$private:pattern的值

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

我們如何在自己的函數/ cmdlet中實現相同的行為?

我們沒有 - DoInvokeReturnAsIs() (和類似的scriptblock調用工具)被標記為internal ,因此只能由System.Management.Automation程序集中包含的類型調用

給出的示例不起作用,因為默認情況下調用函數將進入新范圍。 Where-Object仍將調用過濾器腳本而不輸入過濾器腳本,但該函數的范圍沒有private變量。

有三種方法可以解決這個問題。

將功能放在與呼叫者不同的模塊中

每個模塊都有一個SessionState ,它有自己的SessionStateScope堆棧。 每個ScriptBlock都與解析的SessionState相關聯。

如果調用模塊中定義的函數,則會在該模塊的SessionState創建新范圍,但不會在頂級SessionState 因此,當Where-Object在不輸入新范圍的情況ScriptBlock濾器腳本時,它會在與該ScriptBlock綁定的SessionState的當前范圍上執行此操作。

這有點脆弱,因為如果你想從你的模塊中調用該函數,那么你就不能。 它會有同樣的問題。

使用點源運算符調用該函數

您很可能已經知道用於在不創建新范圍的情況下調用腳本文件的點源運算符( . )。 這也適用於命令名稱和ScriptBlock對象。

. { 'same scope' }
. Foo-MyBar

但是請注意,這將調用函數來自的SessionState的當前范圍內的函數 ,因此您不能依賴. 總是在調用者的當前范圍內執行。 因此,如果使用點源運算符調用與不同SessionState關聯的函數(例如(不同)模塊中定義的函數),則可能會產生意外影響。 創建的變量將持續存在於將來的函數調用中,並且函數本身中定義的任何輔助函數也將持續存在。

寫一個Cmdlet

編譯的命令(cmdlet)在調用時不會創建新的作用域。 您也可以使用類似的API來使用Where-Object (盡管不是完全相同的)

下面是如何使用公共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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM