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


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

工作?

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的脚本块中),因此根据需要,它保持未绑定

注意事项

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

  • 一个更有效的替代方案可以避免这个问题,而且值得注意的是,还避免了错误(从Windows PowerShell v.1.14393.693和PowerShell Core v6.0.0α15开始),在该错误中,局部变量的闭包可以中断,即,当封装脚本/函数具有验证属性参数时,如[ValidateNotNull()],该参数未绑定(未传递值)[1]-是以下更复杂的表达式再次向PetSerAl致敬,Burt_Harris在这里的回答

    $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
    
    • & { ... }使用自己的局部变量创建子作用域
    • $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

这产生:

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]复制:调用.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."
也就是说,试图在闭包中包含未绑定的-bar参数值-$bar变量,然后它显然默认为$null,这违反了它的验证属性
传递有效的-bar值会使问题消失;例如foo -bar ''
将其视为错误的基本原理:如果函数本身在没有-bar参数值的情况下将$bar视为不存在,那么.GetNewClosure()也应该如此

虽然整个方法在给定的例子中似乎被误导了,但正如使其发挥作用的练习一样,关键是在正确的时间控制变量扩展。在foreach循环中,$_为null($_仅在管道中有效)。您需要等到它到达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循环中进行求值。

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

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

您需要另一个循环才能将其绑定到数组中的特定项。话虽如此,我认为使用一系列对象会是一种更干净的方法,但我不知道你到底在处理什么。

相关内容

  • 没有找到相关文章

最新更新