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

时间:2017-02-12 03:28:35

标签: 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

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

Expression = {$_.$p}

工作?

3 个答案:

答案 0 :(得分:4)

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

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

问题您使用变量($p)来访问本身的 工作(例如,$p = 'Year'; Get-Date | % { $_.$p })。

相反,问题是脚本块$p中的 { $_.$p }以后 之前不会被评估。 Format-Table调用,表示相同的固定值用于所有输入对象 - 即此时$p 的值(其中恰好是$p循环中分配给foreach的最后一个值。

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

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

docs(强调添加):

  

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

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

警告

  • 虽然上面使用的.GetNewClosure()很方便,但它具有无效的缺点,即总是捕获所有局部变量,而不仅仅是所需的那些变量。

  • 更有效的替代可避免此问题 - 尤其是可避免错误 (自Windows PowerShell v5.1.14393起) .693和PowerShell Core v6.0.0-alpha.15)其中局部变量的闭包可以中断,即封闭脚本/函数有 参数使用验证属性,例如[ValidateNotNull()] ,该参数未绑定(未传递任何值) [1] - 是以下,显着更复杂的表达帽子的提示再次给PetSerAl和Burt_Harris的回答here

    $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
    
    • & { ... }使用自己的局部变量创建子范围
    • $p = $p然后从继承的值创建本地 $p变量。
      为了概括这种方法,必须为脚本块中引用的每个变量包含这样的语句
    • { $_.$p }.GetNewClosure()然后输出一个脚本块,该块关闭子范围的局部变量(在这种情况下只是$p)。
    • 该错误已被报告为an issue in the PowerShell Core GitHub repository并且已经been fixed - 我不清楚修补程序将发布的版本。
  • 对于简单案例,mjolinor's answer可能会:间接通过扩展字符串创建脚本块合并当时的$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中指定的列标签,同时包含正确的值。

请注意我是如何删除不必要的;个实例,并使用底层cmdlet名称替换内置别名%ft以获得清晰度。我还指定了不同的age值,以更好地证明输出是正确的。

更简单的解决方案,在特定的案例中

要引用属性值 as-is without transformation ,只需使用属性的名称即可计算属性中的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 '' 考虑这个 bug 的基本原理:如果函数本身在没有$bar参数值的情况下将-bar视为不存在,那么{ {1}}。

答案 1 :(得分: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

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

答案 2 :(得分:0)

访问HashTables数组中的任何内容都会有点挑剔,但你的变量扩展会更正如下:

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

您需要另一个循环才能将其绑定到数组中的特定项目。 话虽这么说,我认为使用一系列对象将是一个更清晰的方法 - 但我不知道你正在处理什么,确切。