从模块导出函数时,延迟绑定脚本块不起作用

Delay-bind script block does not work when function is exported from module

我有以下功能:

function PipeScript {
    param(
        [Parameter(ValueFromPipeline)]
        [Object] $InputObject,

        [Object] $ScriptBlock
    )

    process {
        $value = Invoke-Command -ScriptBlock $ScriptBlock
        Write-Host "Script: $value"
    }
}

当我直接在脚本中定义此函数并通过管道输入到其中时,我得到了以下预期结果:

@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }

# Outputs: "Script: Test"

但是当我在模块中定义这个函数并用 Export-ModuleMember -Function PipeScript 导出它时,脚本块中的管道变量 $_ 总是 null:

Import-Module PipeModule
@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }

# Outputs: "Script: "

可在以下位置获得完整复制:https://github.com/lpatalas/DelayBindScriptBlock

有人可以解释这种行为吗?

感谢 PetSerAl 的帮助。

这是一个简单的解决方案,但请注意它在调用者的范围内直接运行脚本块,即它实际上是“点源”,允许修改调用者的变量。

相比之下,您对 Invoke-Command 的使用在调用方范围的 child 范围内运行脚本块 - 如果确实如此,请参阅变体解决方案下面。

“点源”脚本块也是 Where-ObjectForEach-Object 等标准 cmdlet 所做的。

# Define the function in an (in-memory) module.
# An in-memory module is automatically imported.
$null = New-Module {

  function PipeScript {
    param(
      [Parameter(ValueFromPipeline)]
      [Object] $InputObject
      ,
      [scriptblock] $ScriptBlock
    )

    process {

      # Use ForEach-Object to create the automatic $_ variable
      # in the script block's origin scope.
      $value = ForEach-Object -Process $ScriptBlock -InputObject $InputObject

      # Output the value
      "Script: $value"
    }

  }

}

# Test the function:
$var = 42; @{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 43 - the script block ran in the caller's scope.

以上输出字符串 Script: Test43 之后,证明输入对象被视为 $_ 并且点源有效($var 已成功递增在调用者的范围内)。


这里有一个 变体,通过 PowerShell SDK,child 范围内运行脚本块调用者的范围.

如果您不希望脚本块的执行意外修改调用者的变量,这会很有帮助。

这与您使用引擎级延迟绑定脚本块和计算的 属性 功能获得的行为相同 - 尽管 it's unclear whether that behavior was chosen intentionally.

$null = New-Module {

  function PipeScript {
    param(
      [Parameter(ValueFromPipeline)]
      [Object] $InputObject
      ,
      [scriptblock] $ScriptBlock
    )

    process {
      # Use ScriptBlock.InvokeContext() to inject a $_ variable
      # into the child scope that the script block runs in:
      # Creating a custom version of what is normally an *automatic* variable
      # seems hacky, but the docs do state:
      # "The list of variables may include the special variables 
      #  $input, $_ and $this." - see https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext
      $value = $ScriptBlock.InvokeWithContext(
        $null, # extra functions to define (none here)
        [psvariable]::new('_', $InputObject) # actual parameter type is List<PSVariable>
      )
      # Output the value
      "Script: $value"
    }

  }

}

# Test the function:
$var = 42
@{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 42 - unaltered, because the script block ran in a child scope.

以上输出字符串 Script: Test,后跟 42,证明脚本块将输入对象视为 $_ 和变量 $var - 虽然 在脚本块中看到,未修改,因为在子范围中运行。

ScriptBlock.InvokeWithContext() 方法已记录 here


至于为什么你的尝试没有成功

  • 通常,脚本块被绑定到它们创建的范围和范围域(除非它们被明确创建为unbound 脚本块,[scriptblock]::Create('...')).

  • 模块外的范围是默认范围域的一部分。每个模块都有自己的作用域,除了全局作用域,所有作用域中的所有作用域都可以看到,不同作用域中的作用域彼此看不到。

  • 您的脚本块是在默认范围域中创建的,当模块定义的函数调用它时,$_ 在原始范围中查找 ,即在(非模块)caller 作用域中,它未定义,因为自动 $_ 变量是由 PowerShell 按需创建的在 local 范围内,它在封闭模块的范围域中。

  • 通过使用 .InvokeWithContext(),脚本块在调用者范围的 child 范围内运行(与 [=28 一样) =] 和 Invoke-Command 默认情况下),上面的代码 向其中注入自定义 $_ 变量 以便脚本块可以引用它。


为这些场景提供更好的 SDK 支持 正在 GitHub issue #3581 中讨论。