【问题标题】:Why does the scope of variables change depending on if it's a .ps1 or .psm1 file, and how can this be mitigated?为什么变量的范围会根据它是 .ps1 还是 .psm1 文件而改变,如何缓解这种情况?
【发布时间】:2020-09-10 17:44:33
【问题描述】:

我有一个执行脚本块的函数。为方便起见,脚本块不需要显式定义参数,而是可以使用$_$A 来引用输入。

在代码中,这样做是这样的:

$_ = $Value
$A = $Value2
& $ScriptBlock

这整件事都包装在一个函数中。最小的例子:

function F {
    param(
        [ScriptBlock]$ScriptBlock,
        [Object]$Value
        [Object]$Value2
    )

    $_ = $Value
    $A = $Value2
    & $ScriptBlock
}

如果这个函数写在 PowerShell 脚本文件 (.ps1) 中,但使用 Import-Module 导入,F 的行为符合预期:

PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>

但是,当函数写入 PowerShell 模块文件 (.psm1) 并使用 Import-Module 导入时,会出现意外行为:

PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>

使用{$_ + 1} 代替1。似乎$_ 的值改为$null。据推测,某些安全措施限制了$_ 变量的范围或以其他方式保护它。或者,$_ 变量可能是由某个自动过程分配的。无论如何,如果只有 $_ 变量受到影响,第一个不成功的示例将返回 1

理想情况下,该解决方案将涉及显式指定运行脚本块的环境的能力。比如:

Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock

总之,问题是:

  • 为什么模块文件中的脚本块不能访问在调用它们的函数中定义的变量?
  • 是否有一种方法可以在调用脚本块时显式指定可由脚本块访问的变量?
  • 是否有其他不涉及在脚本块中包含显式参数声明的解决方法?

【问题讨论】:

  • 轻松缓解:使用ForEach-Object:$Value |ForEach-Object $ScriptBlock

标签: powershell scope scripting


【解决方案1】:

乱序:

  • 是否有其他不涉及在脚本块中包含显式参数声明的解决方法?

是的,如果您只想填充$_,请使用ForEach-Object

ForEach-Object 在调用者的本地范围内执行,这可以帮助您解决问题 - 除了您不必,因为它还会自动将输入绑定到 $_/$PSItem

# this will work both in module-exported commands and standalone functions
function F {
    param(
        [ScriptBlock]$ScriptBlock,
        [Object]$Value
    )

    ForEach-Object -InputObject $Value -Process $ScriptBlock
}

现在F 将按预期工作:

PS C:\> F -Value 7 -ScriptBlock {$_ * 2}

理想情况下,该解决方案将涉及显式指定运行脚本块的环境的能力。比如:

Invoke-ScriptBlock -Variables @{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock

使用ScriptBlock.InvokeWithContext() 执行脚本块:

$functionsToDefine = @{
  'Do-Stuff' = {
    param($a,$b)
    Write-Host "$a - $b"
  }
}

$variablesToDefine = @(
  [PSVariable]::new("var1", "one")
  [PSVariable]::new("var2", "two")
)

$argumentList = @()

{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)

或者,像你原来的例子一样包装在一个函数中:

function F
{
  param(
    [scriptblock]$ScriptBlock
    [object]$Value
  )

  $ScriptBlock.InvokeWithContext(@{},@([PSVariable]::new('_',$Value)),@())
}

现在您知道如何解决您的问题,让我们回到关于模块作用域的问题。

首先,值得注意的是,您实际上可以使用模块来实现上述目的,但有点相反。

(在下文中,我使用New-Module 定义的内存模块,但描述的模块范围解析行为与从磁盘导入脚本模块时相同)

虽然模块范围“绕过”正常范围解析规则(请参阅下面的解释),但 PowerShell 实际上支持在特定模块范围内的反向显式执行。

只需将模块引用作为第一个参数传递给 & 调用运算符,PowerShell 会将后续参数视为要在所述模块中调用的命令:

# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2

& $twoPlusTwo # yields 4

# let's try it with explicit module-scoped execution
$myEnv = New-Module {
  $two = 2.5
}

& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)

  • 为什么模块文件中的脚本块不能访问在调用它们的函数中定义的变量?
  • 如果可以,为什么$_ 自动变量不能?

因为加载的模块维护状态,而 PowerShell 的实现者希望将模块状态从调用者的环境中分离出来。

您问,为什么这可能有用,为什么一个会排除另一个?

考虑以下示例,一个用于测试奇数的非模块函数:

$two = 2
function Test-IsOdd
{
  param([int]$n)

  return $n % $two -ne 0
}

如果我们在脚本或交互式提示中运行上述语句,随后调用 Test-IsOdd 应该会产生预期的结果:

PS C:\> Test-IsOdd 123
True

到目前为止,一切都很好,但是在这种情况下依赖非本地变量 $two 存在缺陷 - 如果在我们的脚本或 shell 中的某个地方我们不小心重新分配了本地变量 $two,我们可能会中断Test-IsOdd完全:

PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False

这是意料之中的,因为默认情况下,变量范围解析只是在调用堆栈上游荡,直到到达全局范围。

但有时您可能要求在一个或多个函数的执行过程中保持状态,就像我们上面的示例一样。

模块通过遵循稍微不同的范围解析规则来解决这个问题 - 模块导出的函数遵循我们称为模块范围的东西(在到达全局范围之前)。

为了说明这如何解决我们之前的问题,考虑这个相同函数的模块导出版本:

$oddModule = New-Module {
  function Test-IsOdd
  {
    param([int]$n)

    return $n % $two -ne 0
  }

  $two = 2
}

现在,如果我们调用导出的新模块 Test-IsOdd,我们可以预见地得到预期的结果,不管调用者范围内的“污染”:

PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True

这种行为,虽然可能令人惊讶,但基本上用于巩固模块作者和用户之间的隐含契约 - 模块作者不需要过多担心“外面”有什么(调用者会话状态),并且用户可以期望“在那里”(加载的模块的状态)发生的任何事情都能正常工作,而不必担心他们分配给本地范围内的变量的内容。


模块作用域行为不佳documented in the help files,但在 Bruce Payette 的“PowerShell In Action”的第 8 章中有深入的解释 (ISBN:9781633430297)

【讨论】:

  • 谢谢!几个问题:这是否意味着脚本块在全局范围内执行,因此无法访问模块范围的变量?毕竟脚本块使用的变量是在函数中定义的,而不是在模块中定义的。
  • 第二个问题:您概述了使用ForEach-Object 来绑定$_ 变量,但是有没有办法同时绑定另一个变量?我在我的问题中省略了这个,但我的用例需要绑定两个变量。
  • 脚本块可以看到模块范围的变量,但是$_ = $value不在模块范围内执行,它在模块导出函数F的本地范围内执行
  • @schuelermine 如果您需要建议,请用您的实际用例更新您的原始帖子:)
  • 我做到了!谢谢你的信息,虽然
猜你喜欢
  • 2015-03-04
  • 1970-01-01
  • 1970-01-01
  • 2016-12-08
  • 1970-01-01
  • 2022-06-10
  • 1970-01-01
  • 2013-05-24
  • 1970-01-01
相关资源
最近更新 更多