OP 的代码和这个答案使用PSv3+语法。[pscustomobject]
PSv2 不支持将哈希表转换为,但您可以替换[pscustomobject] $_
为New-Object PSCustomObject -Property $_
.
与过去的许多情况一样,PetSerAl以简洁(但非常有帮助)的评论提供了答案;让我详细说明:
您的问题不在于您使用变量 ( $p
) 来访问属性本身,它确实有效(例如,$p = 'Year'; Get-Date | % { $_.$p }
)。
相反,问题是$p
in script 块{ $_.$p }
直到稍后才被评估,在Format-Table
调用的上下文中,这意味着相同的固定值用于所有输入对象- 即该$p
点的值(恰好是循环中分配给$p
的最后一个值)。foreach
最干净和最通用的解决方案是调用.GetNewClosure()
脚本块以在脚本块中绑定$p
到then-current, loop-iteration-specific value。
$format += @{ Label = $p; Expression = { $_.$p }.GetNewClosure() }
从文档中(强调添加;更新:引用的段落已被删除,但仍然适用):
在这种情况下,新的脚本块在定义闭包的范围内的局部变量上关闭。换句话说,局部变量的当前值被捕获并包含在绑定到模块的脚本块中。
请注意,自动变量$_
在循环内未定义foreach
(PowerShell 仅在某些上下文中将其定义为手头的输入对象,例如在传递给管道中的 cmdlet 的脚本块中),因此它根据需要保持为unbound。
注意事项:
虽然.GetNewClosure()
上面使用的方法很方便,但它的缺点是总是捕获所有局部变量,而不仅仅是需要的局部变量;此外,返回的脚本块在为该场合创建的动态(内存中)模块中运行。
避免此问题的更有效的替代方案 - 尤其是还避免了一个错误(从 Windows PowerShell v5.1.14393.693 和 PowerShell Core v6.0.0-alpha.15 开始),其中局部变量的闭包可能会中断,即当封闭的脚本/函数有一个 带有验证属性的参数,例如该参数未绑定(未传递任何值)[ValidateNotNull()]
[1] - 下面是更复杂的表达式,再次向 PetSerAl 致敬,以及 Burt_Harris 的答案
:
$format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
& { ... }
创建一个具有自己的局部变量的子作用域。
$p = $p
然后从其继承的值创建一个局部 变量。
为了概括这种方法,您必须为脚本块中引用的每个变量包含这样的语句。$p
{ $_.$p }.GetNewClosure()
然后输出一个关闭子作用域的局部变量的脚本块(仅$p
在本例中)。
- 该错误已被报告为PowerShell Core GitHub 存储库中的一个问题,并且已得到修复——我不清楚该修复程序将发布哪些版本。
对于简单的情况,mjolinor 的答案可能是:它通过扩展字符串间接创建一个脚本块,该字符串包含当时的当前值,但请注意,该方法难以概括,因为仅对变量值进行字符串化通常不能保证它作为 PowerShell源代码的一部分工作(扩展字符串必须对其进行评估才能转换为脚本块)。$p
把它们放在一起:
# 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
同时包含正确的值。
请注意,为了清楚起见,我如何删除了不必要;
的实例并替换了内置别名%
和ft
底层 cmdlet 名称。我还分配了不同的age
值以更好地证明输出是正确的。
更简单的解决方案,在这种特定情况下:
要按原样引用属性值,无需转换,只需使用属性名称作为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 ''
$bar
-bar
.GetNewClosure()