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

Powershell: Use a variable to reference a property of $_ in a script block

$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}

工作?

访问哈希表数组中的任何内容会有点挑剔,但您的变量扩展已更正如下:

    $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 的代码和此答案使用 PSv3+ 语法。 PSv2 不支持将哈希表转换为 [pscustomobject],但您可以将 [pscustomobject] $_ 替换为 New-Object PSCustomObject -Property $_

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

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

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

最干净和最通用的解决方案是在脚本块上调用 .GetNewClosure()在脚本块中绑定 $pthen-current、loop-iteration-specific 值.

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

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

In this case, the new script block is closed over the local variables in the scope that the closure is defined in. In other words, the current values of the local variables are captured and enclosed inside the script block that is bound to the module.

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

注意事项

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

  • 一个更有效的替代方案 避免了这个问题 - 值得注意的是还避免了一个错误 (截至 Windows PowerShell v5.1.14393.693 和 PowerShell Core v6.0.0-alpha.15)其中局部变量的闭包可以 break ,即当封闭脚本/函数具有 参数 验证属性 例如 [ValidateNotNull()] and 那个参数是 not bound (没有传递值)[1] - 是下面的, 显着更复杂的表达 再次向 PetSerAl 致敬,以及 Burt_Harris 的回答 here :

      $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
    
    • & { ... } 创建一个 子作用域 ,它有自己的局部变量。
    • $p = $p 然后根据其 inherited 值创建一个 local $p 变量。
      要推广这种方法,您必须为脚本块中引用的每个变量包含这样一条语句
    • { $_.$p }.GetNewClosure() 然后输出一个脚本块,关闭子范围的局部变量(在这种情况下只是 $p)。
    • 该错误已报告为 an issue in the PowerShell Core GitHub repository and has since been fixed - 我不清楚修复程序将在哪些版本中发布。
  • 对于简单情况,可能会做:它间接创建一个脚本块通过包含 then-current $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

这产生:

ID Name Age
-- ---- ---
1  abc  3  
2  def  4  

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

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


更简单的解决方案,在这个具体案例中:

要引用一个属性值as-is不转换,就是足以使用 属性 的 name 作为计算的 属性 中的 Expression 条目 (column-formatting 哈希表).换句话说:在这种情况下,您不需要包含 表达式 [scriptblock] 实例 ({ ... }),只需要包含 [string] 值的 属性 姓名.

因此,以下方法也有效:

# Use the property *name* as the 'Expression' entry's value.
$format += @{ Label = $p; Expression = $p }

请注意,这种方法恰好避免了原来的问题,因为$p在赋值时被计算,所以捕获了 loop-iteration-specific 个值。


[1] 重现:调用 .GetNewClosure()function foo { param([ValidateNotNull()] $bar) {}.GetNewClosure() }; foo 失败,错误为 Exception calling "GetNewClosure" with "0" argument(s): "The attribute cannot be added because variable bar with value would no longer be valid."
也就是说,一个尝试在闭包中包含 unbound -bar 参数值 - $bar 变量,这显然默认为 $null,这违反了它的验证属性。
传递有效的 -bar 值会使问题消失;例如,foo -bar ''.
考虑这是一个 bug 的理由:如果 函数本身 在没有 -bar 参数值的情况下处理 $bar因为不存在,所以 .GetNewClosure().

虽然对于给定的示例,整个方法似乎被误导了,但正如使其发挥作用的练习一样,关键是在正确的时间控制变量扩展。在您的 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 循环中进行评估。