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

der*_*rek 4 powershell closures scriptblock

$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
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,我想通过一个变量名访问每个对象的属性。但它不能按预期工作。所以就我而言,如何制作

Expression = {$_.$p}
Run Code Online (Sandbox Code Playgroud)

在职的?

mkl*_*nt0 6

OP 的代码和此答案使用PSv3+语法。[pscustomobject]PSv2 不支持将哈希表转换为 ,但您可以替换[pscustomobject] $_New-Object PSCustomObject -Property $_.

与过去的许多情况一样,PetSerAl对这个问题提供了简洁(但非常有帮助)的答案;让我详细说明:

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

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

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

$format += @{ Label = $p; Expression = { $_.$p }.GetNewClosure() }
Run Code Online (Sandbox Code Playgroud)

文档中(强调添加;更新:引用的段落已被删除,但仍然适用):

在这种情况下,新脚本块在定义闭包的范围内的局部变量上被关闭。换句话说,局部变量的当前值被捕获并包含在绑定到模块的脚本块中。

请注意,自动变量$_foreach循环内未定义(PowerShell 仅在某些上下文中将其定义为手头的输入对象,例如在管道中传递给 cmdlet 的脚本块中),因此它根据需要保持未绑定

注意事项

  • 虽然.GetNewClosure()上面使用的很方便,但它有一个效率低下的缺点,即总是捕获所有局部变量,而不仅仅是需要的局部变量;此外,返回的脚本块在为该场合创建的动态(内存中)模块中运行。

  • 一个更有效率的选择是避免了这个问题-尤其是和也避免了错误(如Windows PowerShell中v5.1.14393.693和PowerShell核心V6.0.0-alpha.15的),其中关闭了局部变量可以突破,即当包围脚本/函数具有 参数验证特性[ValidateNotNull()] 该参数未结合(没有值被传递)[1] -如下,显著更复杂的表达式的帽子提示再次PetSerAl,和Burt_Harris的回答这里

      $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
    
    Run Code Online (Sandbox Code Playgroud)
    • & { ... }创建一个具有自己的局部变量的子作用域
    • $p = $p然后从其继承的值创建一个局部 $p变量。 要推广此方法,您必须为脚本块中引用的每个变量包含这样的语句
    • { $_.$p }.GetNewClosure()然后输出关闭子作用域的局部变量的脚本块(仅$p在本例中)。
    • 该错误已被报告为PowerShell Core GitHub 存储库中的一个问题,此后已得到修复- 我不清楚修复将在哪个版本中发布。
  • 对于简单的情况,mjolinor 的答案可能会这样做:它通过扩展字符串间接创建一个脚本块,该字符串从字面上合并了当时的当前$p值,但请注意,该方法很难概括,因为仅将变量值字符串化通常不能保证它作为 PowerShell源代码的一部分工作(扩展后的字符串必须计算为它才能转换为脚本块)。

把它们放在一起:

# 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
Run Code Online (Sandbox Code Playgroud)

这产生:

$format += @{ Label = $p; Expression = { $_.$p }.GetNewClosure() }
Run Code Online (Sandbox Code Playgroud)

根据需要:输出列使用指定的列标签,$properties同时包含正确的值。

请注意,为了清楚起见,我如何删除不必要的;实例并替换内置别名%ft底层 cmdlet 名称。我还分配了不同的age值以更好地证明输出是正确的。


更简单的解决方案,在这种特定情况下:

要按原样引用属性值,无需转换,使用属性名称作为Expression计算属性(列格式哈希表)中条目就足够了。换句话说:在这种情况下,您不需要[scriptblock]包含表达式的实例( { ... }),只需要[string]包含属性名称的值。

因此,以下方法也有效:

# Use the property *name* as the 'Expression' entry's value.
$format += @{ Label = $p; Expression = $p }
Run Code Online (Sandbox Code Playgroud)

请注意,这种方法恰好避免了原始问题,因为在赋值时$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()

  • 变量不能违反验证属性。这种违规会在赋值`[ValidateNotNull()] $o = $null` 时被发现,或者当你尝试添加属性 `$o = $null; 时。(gv o).Attributes.Add([ValidateNotNull]::new())`。但是未使用的参数即使违反了验证属性也会有默认值。当`GetNewClosure()` 捕获这样的参数时,它就会失败。应该已经在 Stack Overflow 上提出了这个问题。 (2认同)