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