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

Powershell:使用变量引用脚本块中的$\属性,powershell,closures,scriptblock,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

在上面的示例中,我希望通过变量名访问每个对象的属性。但它不能像预期的那样工作。那么在我的情况下,如何

$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]$\u
替换为
新对象pscustomobject-属性$\u

与过去的许多案例一样,他以简短(但非常有用)的评论给出了问题的答案;让我详细说明:

您的问题不在于使用变量(
$p
)访问本身有效的属性(
$p='Year';Get Date |%{$.$p}

相反,问题在于,脚本块
{$$.$p}
中的
$p
直到稍后才会在
格式表调用的上下文中进行计算,这意味着对所有输入对象使用相同的固定值,即此时
$p
的值(这恰好是在
foreach
循环中分配给
$p
的最后一个值)

最干净、最通用的解决方案是调用脚本块上的
.GetNewClosure()
,将脚本块中的
$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))
        }
    }
}
从(强调添加;更新:引用的段落已被删除,但仍然适用):

在这种情况下,新脚本块在定义闭包的范围内通过局部变量关闭。换句话说,局部变量的当前值被捕获并封装在绑定到模块的脚本块内

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

注意事项

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

  • 一个更有效的替代方案,可以避免此问题,并且显著地可以避免错误(从Windows PowerShell v5.1.14393.693和PowerShell Core v6.0.0-alpha.15开始)在这种情况下,局部变量上的闭包可能会中断,即当封闭脚本/函数有一个带有验证属性的参数,如
    [ValidateNotNull()]
    ,并且该参数未绑定(未传递任何值)
    [1]-下面是PetSerAl和Burt_Harris的答案中更复杂的表达 :

    • &{…}
      创建一个子作用域,该作用域具有自己的局部变量
    • $p=$p
      然后根据继承的值创建一个本地
      $p
      变量。
      为了推广这种方法,必须为脚本块中引用的每个变量包含这样的语句
    • {$\.$p}.GetNewClosure()
      然后输出一个脚本块,该脚本块关闭子作用域的局部变量(在本例中仅为
      $p
    • 这个bug已经被报道过了,从那以后也一直如此——我不清楚修复程序将发布什么版本
  • 对于简单的情况,可以这样做:它通过一个扩展字符串间接创建一个脚本块,该字符串包含了当时的
    $p
    值,但请注意,这种方法很难推广,因为仅仅字符串化变量值通常不能保证它作为PowerShell源代码的一部分工作(为了转换为脚本块,扩展字符串必须计算为该值)

总而言之:

  $format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }
这将产生:

ID姓名年龄
-- ---- ---
1 abc 3
2 def 4
根据需要:输出列使用
$properties
中指定的列标签,同时包含正确的值

请注意,为了清晰起见,我删除了不必要的
实例,并使用基础cmdlet名称替换了内置别名
%
ft
。我还指定了不同的
age
值,以更好地证明输出是正确的


更简单的解决方案,在此特定情况下:

要按原样引用属性值而不进行转换,在计算属性(列格式哈希表)中使用属性名称作为
表达式
条目
就足够了。换句话说,在这种情况下,您不需要包含表达式的
[scriptblock]
实例(
{…}
),仅包含属性名称的
[string]

因此,以下措施也会奏效:

# 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
请注意,这种方法恰好避免了原始问题,因为
$p
是在赋值时计算的,因此捕获循环迭代特定的值


[1] 要重现:
函数foo{param([ValidateNotNull()]$bar){}.GetNewClosure()};调用
.GetNewClosure()
时foo
失败,错误
异常调用带有“0”参数的“GetNewClosure”:
# 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
# Use the property *name* as the 'Expression' entry's value.
$format += @{ Label = $p; Expression = $p }
$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