![](/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.