Powershell:在脚本块中使用变量引用$ _的属性

5
$var =@(  @{id="1"; name="abc"; age="1"; },
          @{id="2"; name="def"; age="2"; } );
$properties = @("ID","Name","Age") ;
$format = @();
foreach ($p  in $properties)
{
    $format += @{label=$p ; Expression = {$_.$p}} #$_.$p is not working!
}
$var |% { [PSCustomObject]$_  } | ft $format

在上面的例子中,我想通过变量名访问每个对象的属性。但是它不能像预期的那样工作。那么在我的情况下,如何使其正常工作呢?

Expression = {$_.$p}

工作正常吗?


1
你可以直接使用 Expression = $p - user4003407
3
如果你真的想要在 Expression 中使用脚本块,那么你可以使用 Expression = &{$p=$p; {$_.$p}.GetNewClosure()}。请注意,这里的翻译保持了原文的意思和语法结构,并力求让内容更加通俗易懂。 - user4003407
1
@mjolinor,对我来说它很好用。你能展示一些证据表明在这种情况下它不能正常工作吗? - user4003407
1
@mjolinor 奇怪的是,重新关闭一个现有的脚本块所需的时间比使用可扩展字符串创建全新的脚本块所需的时间更长。 针对哪个脚本块大小?我预计 GetNewClosure() 的时间与脚本块大小无关。而 [ScriptBlock]::Create() 应该会随着脚本块大小的增加而花费更多的时间。 - user4003407
@PetSerAl - 诚然,这只是一个小的脚本块(可能不到20个字符,仅涉及单个变量)。 - mjolinor
显示剩余3条评论
3个回答

7

OP的代码和这个答案都使用了PSv3+语法。在PSv2中不支持将哈希表强制转换为[pscustomobject],但是您可以将[pscustomobject] $_替换为New-Object PSCustomObject -Property $_

就像过去的许多情况一样,PetSerAl在问题的简洁(但非常有用)评论中提供了答案;我来详细说明一下:

您的问题不在于您使用变量($p)本身来访问属性,这是有效的(例如,$ p ='Year'; Get-Date | %{ $_. $ P } )。

相反,问题在于脚本块{ $_. $ P }中的$ p 直到稍后才被评估,在Format-Table调用的上下文中,这意味着对于所有输入对象使用相同的固定值,即$ p 的值在那一点上(这恰好是在foreach循环中分配给$ p 的最后一个值)。

最干净和最通用的解决方案是在脚本块上调用.GetNewClosure(),以将脚本块中的$ p 绑定到当前、循环迭代特定值

$format += @{ Label = $p; Expression = { $_.$p }.GetNewClosure() }

根据文档(已经强调; 更新: 引用的段落已被删除,但仍适用):
在这种情况下,新的脚本块被关闭在定义闭包的范围内的局部变量上。换句话说,局部变量的当前值被捕获并封闭在绑定到模块的脚本块中
请注意,在foreach循环内部,自动变量$_未定义(PowerShell仅在某些上下文中将其定义为手头的输入对象,例如在传递给管道中的cmdlet的脚本块中),因此它保持未绑定的状态,正如所期望的那样。 注意事项:
  • While .GetNewClosure() as used above is convenient, it has the inefficiency drawback of invariably capturing all local variables, not just the one(s) needed; also, the returned script block runs in a dynamic (in-memory) module created for the occasion.

  • A more efficient alternative that avoids this problem - and notably also avoids a bug (as of Windows PowerShell v5.1.14393.693 and PowerShell Core v6.0.0-alpha.15) in which the closure over the local variables can break, namely when the enclosing script / function has a parameter with validation attributes such as [ValidateNotNull()] and that parameter is not bound (no value is passed)[1] - is the following, significantly more complex expression Tip of the hat again to PetSerAl, and Burt_Harris's answer here :

      $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
    
    • & { ... } creates a child scope with its own local variables.
    • $p = $p then creates a local $p variable from its inherited value.
      To generalize this approach, you must include such a statement for each variable referenced in the script block.
    • { $_.$p }.GetNewClosure() then outputs a script block that closes over the child scope's local variables (just $p in this case).
    • The bug has been reported as an issue in the PowerShell Core GitHub repository and has since been fixed - it's unclear to me in what versions the fix will ship.
  • For simple cases, mjolinor's answer may do: it indirectly creates a script block via an expanded string that incorporates the then-current $p value literally, but note that the approach is tricky to generalize, because just stringifying a variable value doesn't generally guarantee that it works as part of PowerShell source code (which the expanded string must evaluate to in order to be converted to a script block).

将所有的东西放在一起:
# Sample array of hashtables.
# Each hashtable will be converted to a custom object so that it can
# be used with Format-Table.
$var = @(  
          @{id="1"; name="abc"; age="3" }
          @{id="2"; name="def"; age="4" }
       )

# The array of properties to output, which also serve as
# the case-exact column headers.
$properties = @("ID", "Name", "Age")

# Construct the array of calculated properties to use with Format-Table: 
# an array of output-column-defining hashtables.
$format = @()
foreach ($p in $properties)
{
    # IMPORTANT: Call .GetNewClosure() on the script block
    #            to capture the current value of $p.
    $format += @{ Label = $p; Expression = { $_.$p }.GetNewClosure() }
    # OR: For efficiency and full robustness (see above):
    # $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
}

$var | ForEach-Object { [pscustomobject] $_ } | Format-Table $format

这将产生以下结果:
ID Name Age
-- ---- ---
1  abc  3  
2  def  4  

按照要求翻译如下:

按预期:输出列使用在$properties中指定的列标签,同时包含正确的值。

请注意,我已删除不必要的;实例,并用易于理解的底层cmdlet名称替换了内置别名%ft。 我还分配了不同的age值,以更好地展示输出是正确的。


在这个特定案例中,有一个更简单的解决方案:

为了引用一个属性值“原样”不需要转换,只需在计算属性(列格式散列表)的Expression条目中使用属性的名称即可。 换句话说:在这种情况下,您不需要包含包含表达式[scriptblock]实例({...}),只需要一个包含属性名称[string]值。

因此,以下内容也可以正常工作:
# Use the property *name* as the 'Expression' entry's value.
$format += @{ Label = $p; Expression = $p }

请注意,这种方法恰好可以避免原始问题,因为$p赋值时被评估,所以捕获了循环迭代特定的值。

[1] 复现: function foo { param([ValidateNotNull()] $bar) {}.GetNewClosure() }; foo 在调用.GetNewClosure()时失败,出现错误Exception calling "GetNewClosure" with "0" argument(s): "The attribute cannot be added because variable bar with value would no longer be valid."
也就是说,尝试将未绑定的-bar参数值-$bar变量-纳入闭包中,显然会默认为$null,从而违反其验证属性。
传递一个有效的-bar值可以解决问题;例如,foo -bar''
考虑将其视为错误的理由:如果函数本身在没有-bar参数值的情况下将$bar视为不存在,则.GetNewClosure()也应该如此。


@PetSerAl:顺便说一下:似乎这个属性只在_parameters_中有问题,而不是变量;例如,[ValidateNotNull()] $o = ''。你知道为什么吗? - mklement0
2
变量不能违反验证属性。这样的违规将在赋值[ValidateNotNull()] $o = $null时被捕获,或者当您尝试添加属性$o = $null; (gv o).Attributes.Add([ValidateNotNull]::new())时被捕获。但是,即使未使用的参数违反了验证属性,它们将具有默认值。当GetNewClosure()捕获此类参数时,它会失败。在Stack Overflow上应该已经有人问过与此相关的问题。 - user4003407
1
请注意,变量捕获的源代码位于 https://github.com/PowerShell/PowerShell/blob/02b5f357a20e6dee9f8e60e3adb9025be3c94490/src/System.Management.Automation/engine/Modules/PSModuleInfo.cs 中名为CaptureLocals()的方法中。 - Burt_Harris
1
关闭错误是一个顽固的小虫子在5.14393中。我在一个模块的Register-ArgumentCompleter的scriptblock参数中遇到了它。这个模块本身加载和运行都很好,无论是会话还是任何CLI导入,但当我运行我的备份脚本,执行Remove-Module和Import-Module -Force(以加载更改),它在"update"变量上产生了这个错误(可能是在Register-ArgumentCompleter的某个地方)。无法修复它,所以我只是捕获并抑制它,因为参数完成失败对于备份来说不相关,而且我很快就会失去对系统的访问权限。 - Blaisem

1
虽然这种方法在给定的示例中似乎是错误的,但只要作为使其工作的练习,关键就是在正确的时间控制变量扩展。在你的foreach循环中,$_是空的($_只在管道中有效)。你需要等到它到达Foreach-Object循环时再尝试评估它。
这似乎可以通过最少的重构来实现:
$var =@(  @{id="1"; name="abc"; age="1"; },
      @{id="2"; name="def"; age="2"; } );
$properties = @("ID","Name","Age") ;
$format = @();
foreach ($p  in $properties)
{
    $format += @{label=$p ; Expression = [scriptblock]::create("`$`_.$p")} 
}
$var | % { [PSCustomObject] $_ } | ft $format

从可扩展字符串创建脚本块将允许$p为每个属性名称扩展。转义$_将使其保持为字符串中的文字,直到在ForEach-Object循环中呈现为脚本块并进行评估。

0

访问哈希表数组中的任何内容都会有些棘手,但您可以通过以下方式来更正变量扩展:

    $var =@(  @{id="1"; name="Sally"; age="11"; },
          @{id="2"; name="George"; age="12"; } );
$properties = "ID","Name","Age"
$format = @();

$Var | ForEach-Object{
    foreach ($p  in $properties){
        $format += @{
            $p = $($_.($p))
        }
    }
}

你需要另一个循环来将其绑定到数组中的特定项。 话虽如此,我认为使用对象数组会是一种更清晰的方法 - 但我不知道你具体在处理什么。

我们(现在,可能是在你回答之后)知道OP想要$format包含_计算属性_以便稍后与Format-Table一起使用,因此这样做行不通。 - mklement0

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