首页 > 解决方案 > 从模块导出函数时,延迟绑定脚本块不起作用

问题描述

我有以下功能:

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

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

标签: powershellscopepowershell-core

解决方案


PetSerAl 致敬,感谢他的所有帮助。

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

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

“点源”脚本块也是标准 cmdletWhere-ObjectForEach-Object执行的操作。

# 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,在调用者作用域的作用域中运行脚本块

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

这与使用引擎级延迟绑定脚本块和计算属性功能获得的行为相同 - 尽管尚不清楚该行为是否是故意选择的

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

上面的输出 string Script: Test,后跟42,证明脚本块将输入对象视为$_和该变量$var- 尽管在脚本块中看到,但由于在子范围内运行而没有修改。

ScriptBlock.InvokeWithContext()方法记录在这里


至于为什么你的尝试没有奏效

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

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

  • 您的脚本块是在默认范围域中创建的,当模块定义的函数调用它时,会在 origin 范围内$_查找,即在(非模块)调用方范围内,它没有被定义,因为自动变量是由 PowerShell 在本地范围内按需创建的,它位于封闭模块的范围域中。$_

  • 通过使用.InvokeWithContext(),脚本块在调用者作用域的.Invoke()作用域中运行(与Invoke-Command默认情况下一样),上面的代码将自定义$_变量注入其中,以便脚本块可以引用它。


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


推荐阅读