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

kt06eoxx  于 9个月前  发布在  Shell
关注(0)|答案(3)|浏览(127)
$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}


工作吗

4ioopgfo

4ioopgfo1#

OP的代码和这个答案使用PSv 3 +语法。PSv 2不支持将哈希表转换为[pscustomobject],但您可以将[pscustomobject] $_替换为New-Object PSCustomObject -Property $_
与过去的许多情况一样,PetSerAl以简洁(但非常有用)的评论方式提供了答案;让我详细说明:
你的
问题 * 不是 * 你正在使用一个变量($p)来访问一个属性 * 本身 ,它 * 确实 * 工作(例如,$p = 'Year'; Get-Date | % { $_.$p })。
相反,问题在于脚本块{ $_.$p }中的
*$p直到 * 稍后 * 才在Format-Table调用的上下文中进行计算,这意味着
相同,固定值用于所有输入对象**-即$p * 在该点的值 (这恰好是在foreach循环中分配给$p的最后一个值)。
最干净和最通用的解决方案是在脚本块上调用
*.GetNewClosure()**,将脚本块中的$p绑定到 then-current,循环迭代特定的值

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

字符串
docs(强调添加; * 更新 :引用的段落已被删除,但仍然适用):
在这种情况下,新的脚本块在定义闭包的范围内对 * local
variables进行闭包。换句话说,*local 变量的当前值被捕获并封装在绑定到模块的脚本块内。
请注意,automatic 变量$_foreach循环中是未定义的(PowerShell仅在某些上下文中将其定义为手头的输入对象,例如在管道中传递给小程序的脚本块中),因此它仍然保持 * 未绑定 *。

注意事项

  • 虽然上面使用的.GetNewClosure()很方便,但它有效率低下的缺点,总是捕获 * 所有 * 局部变量,而不仅仅是所需的变量;此外,返回的脚本块在为这种情况创建的动态(内存中)模块中运行。
  • 一个更有效的替代方案,避免了这个问题-特别是避免了 Windows PowerShell 中的一个 *bug *,其中局部变量上的闭包可以 break,也就是说,当封装的脚本/函数具有 * 参数 *,该参数具有 * 验证属性 *,如[ValidateNotNull()] *,并且 * 该参数 * 未绑定 * 时,(没有值被传递)[1] -是下面的,明显更复杂的表达式再次向PetSerAl提示帽子,Burt_Harris的答案here
$format += @{ Label = $p; Expression = & { $p = $p; { $_.$p }.GetNewClosure() } }

  • & { ... }创建一个带有自己的局部变量的 * 子作用域 *。
  • 然后$p = $p从它的 inherited 值创建一个 local$p变量。

为了推广这种方法,* 您必须为脚本块中引用的每个变量都包含这样的语句 *。

  • 然后{ $_.$p }.GetNewClosure()输出一个脚本块,它覆盖子作用域的局部变量(在本例中仅为$p)。
  • 该错误最初报告为GitHub issue #3144,自PowerShell (Core) 7+中的been fixed以来一直存在(但不会在 Windows PowerShell 中修复)。
  • 对于 * 简单 * 的情况,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中指定的列标签,同时包含正确的值。
请注意,为了清楚起见,我删除了不必要的;示例,并将内置别名%ft替换为底层的小程序名称。我还分配了不同的age值,以更好地证明输出是正确的。

更简单的解决方案,在此 * 具体 * 情况下:
若要引用属性值 * 按原样 不进行转换 *,则在计算属性中使用属性的 * 名称 * 作为Expression条目即可(列格式化哈希表)。换句话说:你不需要一个包含 expression[scriptblock]示例,在这种情况下({ ... }),只需要一个包含属性 name[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."
也就是说,试图在闭包中包含 unbound-bar参数值($bar变量),这显然会默认为$null,这违反了它的验证属性。
传递一个有效的-bar值可以解决问题;例如foo -bar ''
认为这是一个 bug 的理由是:如果 * 函数本身 * 在没有-bar参数值的情况下将$bar视为不存在,那么.GetNewClosure()也应该如此。

dauxcl2d

dauxcl2d2#

虽然整个方法对于给定的例子似乎是错误的,但正如使其工作的练习一样,关键是在正确的时间控制变量扩展。在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循环中进行计算。

j8yoct9x

j8yoct9x3#

在一个Hash表数组中添加任何内容都会有点挑剔,但是你的变量扩展是这样纠正的:

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

字符串
你需要另一个循环才能将它绑定到数组中的特定项。也就是说,我认为使用Object数组是一种更简洁的方法-但我不知道你在处理什么,确切地说。

相关问题