![](/img/trans.png)
[英]powershell use of where-object within script function not working
[英]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在內部調用過濾器腳本而不將其限制在自己的本地范圍內 ( _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);
}
}
// ...
}
我們如何在自己的函數/ 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)在調用時不會創建新的作用域。 您也可以使用類似的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.