从一个模块中使用的函数中的$_变量为空 (PowerShell)

13

这里有一个问题要问你 ;)

我有这个函数:

function Set-DbFile {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [System.IO.FileInfo[]]
        $InputObject,
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [scriptblock]
        $Properties
    )
    process {
        $InputObject | % { 
            Write-Host `nInside. Storing $_.Name
            $props = & $Properties
            Write-Host '  properties for the file are: ' -nonew
            write-Host ($props.GetEnumerator()| %{"{0}-{1}" -f $_.key,$_.Value})
        }
    }
}

查看$Properties。它应该针对每个文件进行评估,然后进一步处理文件和属性。

如何使用的示例可能是:

Get-ChildItem c:\windows |
    ? { !$_.PsIsContainer } |
    Set-DbFile -prop { 
        Write-Host Creating properties for $_.FullName
        @{Name=$_.Name } # any other properties based on the file
    }

当我将函数 Set-dbFile 复制粘贴到命令行并运行示例代码时,一切都正常。

但是,当我将函数存储在一个模块中,导入它并运行示例时,$_ 变量为空。 有人知道为什么吗? 如何解决? (欢迎其他解决方案)


脚本/在命令行中输入定义的函数的结果:

Inside. Storing adsvw.ini
Creating properties for C:\windows\adsvw.ini
  properties for the file are: Name-adsvw.ini

Inside. Storing ARJ.PIF
Creating properties for C:\windows\ARJ.PIF
  properties for the file are: Name-ARJ.PIF
....

在模块中定义的函数的结果:

Inside. Storing adsvw.ini
Creating properties for
  properties for the file are: Name-

Inside. Storing ARJ.PIF
Creating properties for
  properties for the file are: Name- 
....
3个回答

6
这里的问题在于作用域层次结构。如果您定义了两个函数,例如...
function F1{
    $test="Hello"
    F2
}
function F2{
    $test
}

然后,由于从F1的范围调用了F2,因此F2将继承F1的变量范围。如果您在模块中定义函数F2并导出该函数,则$test变量不可用,因为该模块具有自己的作用域树。请参见Powershell语言规范(第3.5.6节):
在您的情况下,当前节点变量在本地作用域中定义,因此它不会在模块作用域中存活,因为它位于具有不同作用域根的不同树中(除了全局变量)。
引用Powershell语言规范(第4.3.7节)中关于GetNewClosure()方法的文本:
检索绑定到模块的脚本块。任何在调用者上下文中的局部变量都将被复制到模块中。
因此,GetNewClosure()能够很好地起到作用,因为它桥接了本地作用域/模块分隔。希望这可以帮助您。

5

看起来GetNewClosure()是一个不错的解决方法,但它会改变脚本块查看这些变量的方式。将$_作为参数传递给脚本块也可以。

这与正常的作用域问题(例如全局与局部)无关,但一开始看起来似乎是这样。以下是我非常简化的复制和一些解释:

script.ps1 用于正常的点分源:

function test-script([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript
}

用于导入的Module\MyTest\MyTest.psm1

function test-module([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript
}

function test-module-with-closure([scriptblock]$myscript){
    $message = "inside"
    &{write-host "`$message from $message"}    
    &$myscript.getnewclosure()
}

调用和输出:

» . .\script.ps1

» import-module mytest

» $message = "outside"

» $block = {write-host "`$message from $message (inside?)"}

» test-script $block
$message from inside
$message from inside (inside?)

» test-module $block
$message from inside
$message from outside (inside?)

» test-module-with-closure $block
$message from inside
$message from inside (inside?)

因为这引起了我的好奇心,所以我开始四处寻找,并发现了一些有趣的事情。
这个问题与这个问答、以及这个错误报告链接中的内容几乎完全相同。还有其他一些我发现的博客文章也是如此。但尽管它被报告为一个错误,我并不认为如此。
关于作用域方面,about_Scopes页面有以下说明:(w:)
...

Restricting Without Scope

  A few Windows PowerShell concepts are similar to scope or interact with 
  scope. These concepts may be confused with scope or the behavior of scope.

  Sessions, modules, and nested prompts are self-contained environments,
  but they are not child scopes of the global scope in the session.

  ...

  Modules:
    ...

    The privacy of a module behaves like a scope, but adding a module
    to a session does not change the scope. And, the module does not have
    its own scope, although the scripts in the module, like all Windows
    PowerShell scripts, do have their own scope. 

现在我理解了这个行为,但是上面和其他一些实验使我得出了结论:

  • 如果我们在脚本块中将$message更改为$local:message,那么所有3个测试都有一个空格,因为$message在脚本块的本地作用域中未定义。
  • 如果我们使用$global:message,则所有3个测试都打印outside
  • 如果我们使用$script:message,前两个测试打印outside,最后一个测试打印inside

然后我也在about_Scopes中读到了这个内容:

Numbered Scopes:
    You can refer to scopes by name or by a number that
    describes the relative position of one scope to another.
    Scope 0 represents the current, or local, scope. Scope 1
    indicates the immediate parent scope. Scope 2 indicates the
    parent of the parent scope, and so on. Numbered scopes
    are useful if you have created many recursive
    scopes.
  • 如果我们使用$((get-variable -name message -scope 1).value)尝试从直接父级作用域获取值,会发生什么?我们仍然得到 outside而不是inside

此时对我来说已经很清楚了,会话和模块具有它们自己的声明范围或上下文(至少对于脚本块来说是这样)。脚本块在声明它们的环境中充当匿名函数,直到您在其上调用GetNewClosure(),此时它们将内部化它们引用的同名变量的副本,在调用GetNewClosure()的作用域中使用这些副本(首先使用局部变量,然后使用全局变量)。快速演示:

$message = 'first message'
$sb = {write-host $message}
&$sb
#output: first message
$message = 'second message'
&$sb
#output: second message
$sb = $sb.getnewclosure()
$message = 'third message'
&$sb
#output: second message

我希望这可以帮到你。
补充:关于设计。
JasonMArcher的评论让我想到了传递到模块中的脚本块的设计问题。在你提问的代码中,即使使用GetNewClosure()解决方法,你也必须知道脚本块将在哪个变量名称中执行才能使其起作用。
另一方面,如果你将参数传递给脚本块,并将$_作为参数传递给它,脚本块就不需要知道变量名称,它只需要知道将传递特定类型的参数。因此,你的模块将使用$props = & $Properties $_而不是$props = & $Properties.GetNewClosure(),并且你的脚本块将更像这样:
{ (param [System.IO.FileInfo]$fileinfo)
    Write-Host Creating properties for $fileinfo.FullName
    @{Name=$fileinfo.Name } # any other properties based on the file
}

请参考CosmosKey的回答以获得进一步的解释。

我已经多次遇到模块及其范围/环境方面的问题,因此我认为这是 PowerShell 的一个设计不太好的特性。感谢您的解释。 - stej
1
当在不同的作用域中使用自动变量时,我建议避免这样做。 - JasonMArcher
抱歉,我认为这个答案有点错误。 @stej 应该真正授予我这些积分。 :) 你说它与范围无关,实际上与范围有关,确切地说是范围树。模块有自己的范围树,因此会出现这种行为。正如我在下面的答案中指出的那样,这清楚地写在 Powershell 语言规范中。 - CosmosKey
我在那个句子中澄清了“范围”的意思,因为当时我并不知道“范围树”。 - Joel B Fant

1

我相信在运行它之前,您需要在该脚本块上调用getnewclosure()。当从脚本文件或模块调用时,脚本块在编译时被评估。但是,在控制台中工作时,不存在“编译时间”。它在运行时被评估,因此在模块中与在控制台中的行为不同。


GetNewClosure() 这样的代码 Set-DbFile -prop {...}.GetNewClosure() 并没有什么用。我猜这些模块背后肯定有一些魔法。 - stej
准备去工作了,但有机会时我会进行一些测试。所有的症状似乎都指向该脚本块的评估。 - mjolinor
$props = & $Properties.getnewclosure() - getnewclosure()会根据脚本块中使用的任何变量的当前值重新评估脚本块。为了在每个新值的$_下重新评估它,需要在process块内调用getnewclosure。 - mjolinor
是的,& $Properties.GetNewClosure() 可以给出正确的结果。谢谢,已点赞。我会保持开放状态,因为我真的很想知道模块内和模块外函数之间的区别是什么。 - stej
是的,我以前见过这个。没有答案。当我从嵌套函数转换为非嵌套函数时,在一个模块中遇到了类似的问题,并通过使用$Script:Properties来解决它。 - Doug Finke

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接