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

时间:2018-11-15 19:19:16

标签: powershell scope powershell-core

我具有以下功能:

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

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

1 个答案:

答案 0 :(得分:0)

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-尽管 由于在子作用域中运行,因此未对 进行修改。

here中记录了ScriptBlock.InvokeWithContext()方法。


关于为什么您的尝试不起作用

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

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

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

  • 通过使用.InvokeWithContext(),脚本块在调用者作用域的 child 作用域中运行(与.Invoke()和{{1}一样) }),上面的代码向其中注入一个自定义Invoke-Command变量,以便脚本块可以引用它。


this GitHub issue中将讨论

为这些情况提供更好的SDK支持