Powershell:使用变量在脚本块中引用$ _的属性 [英] Powershell: Use a variable to reference a property of $_ in a script block

查看:117
本文介绍了Powershell:使用变量在脚本块中引用$ _的属性的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

  $ var = @(@ {id = 1; name = abc; age = 1;},
@ {id = 2 ; name = def; age = 2;});
$ properties = @( ID,名称,年龄);
$ format = @();
foreach($ properties中的$ p)
{
$ format + = @ {label = $ p;表达式= {$ _。$ p}}#$ _。$ p不起作用!
}
$ var |%{[PSCustomObject] $ _} | ft $ format

在上面的示例中,我想通过变量名称访问每个对象的属性。但是它不能按预期工作。因此,在我的情况下,如何使

  Expression = {$ _。$ p} 

正在工作?

解决方案

OP的代码和此答案使用 PSv3 + 语法。 PSv2不支持将哈希表强制转换为 [pscustomobject] ,但是您可以将 [pscustomobject] $ _ 替换为新对象PSCustomObject-属性$ _



与过去的许多情况一样, PetSerAl 提供了对该问题的简短(但非常有帮助)的评论;让我详细说明:



您的问题不是 ,您正在使用变量( $ p )来访问本身 的属性(例如, $ p ='Year') ; Get-Date |%{$ _。$ p} )。



相反,问题是 <$ c $直到以后 {$ _。$ p} 中的c> $ p >,在 Format-Table 调用的上下文中,这意味着所有输入对象均使用相同的固定值-即该值当时 $ p (恰好是分配给 $ p foreach 循环中找到$ c>。



最干净,最通用的解决方案是调用 .GetNewClosure() 在脚本块中绑定 $ p then-current ,特定于循环迭代的值

  $ format + = @ {标签= $ p;表达式= {$ _。$ p} .GetNewClosure()} 

来自 docs (已添加重点):


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


请注意,自动变量 $ _ foreach 循环中未定义(PowerShell仅在某些上下文中将其定义为手边的输入对象,例如在传递给管道中cmdlet的脚本块中),因此它保持 unbound (按需)。



注意事项




  • Whi如上使用的le .GetNewClosure()很方便,它具有效率低下的缺点,它总是捕获所有局部变量,而不仅仅是捕获一个


  • 一个更有效的替代方案,可以避免此问题-并且特别是避免 bug em> (自Windows PowerShell v5.1.14393.693和PowerShell Core v6.0.0-alpha.15起),其中对局部变量的关闭可以 break ,即在关闭脚本时/函数具有 参数验证属性,例如 [ValidateNotNull()] 该参数为未绑定(未传递任何值) [1] -以下是更复杂的表达式再次向PetSerAl致敬,然后Burt_Harris的回答此处

      $ format + = @ {标签= $ p;表达式=& {$ p = $ p; {$ _。$ p} .GetNewClosure()}} 




    • & {...} 创建一个具有自己局部变量的子范围

    • $ p = $ p 然后根据其继承的值创建一个 local $ p 变量。

      要推广这种方法,您必须为脚本块中引用的每个变量包括这样的语句

    • {$ _。$ p} .GetNewClosure()然后输出一个脚本块,该脚本块关闭子作用域的局部变量(本例中为 $ p ) 。

    • 该错误已报告为 PowerShell Core GitHub存储库,并且自已修复-尚不清楚


  • 对于简单情况, mjolinor的答案可能会做到:它间接通过扩展字符串创建一个脚本块 ,该字符串并入了当时最新的 $ p 从字面上理解,但是请注意,这种方法难以一概而论,因为仅对变量值进行字符串化处理通常并不能保证它可以作为PowerShell 源代码的一部分(扩展后的字符串必须评估后才能转换为脚本块)。




放入全部一起:

 #哈希表的示例数组。 
#每个哈希表将转换为自定义对象,以便它可以与Format-Table一起使用。
$ var = @(
@ {id = 1; name = abc; age = 3}
@ {id = 2; name = def ; age = 4}


#要输出的属性数组,也可以用作
#大小写精确的列标题。
$ properties = @( ID, Name, Age)

#构造要与Format-Table一起使用的计算属性数组:
#一个数组定义输出列的哈希表。
$ format = @()
foreach($ properties中的$ p)
{
#重要:在脚本块
#上调用.GetNewClosure()进行捕获$ p的当前值。
$ format + = @ {标签= $ p;表达式= {$ _。$ p} .GetNewClosure()}
#OR:为了提高效率和增强鲁棒性(请参见上文):
#$ format + = @ {Label = $ p;表达式=& {$ p = $ p; {$ _。$ p} .GetNewClosure()}}
}

$ var | ForEach-Object {[pscustomobject] $ _} |格式表$ format

这将产生:



< pre class = lang-none prettyprint-override> ID名称年龄
----- ---
1 abc 3
2 def 4

根据需要:输出列使用在 $ properties

请注意,我如何删除了不必要的; 实例为了清晰起见,将内置别名 ft 替换为基础cmdlet名称。我还分配了不同的 age 值,以更好地证明输出正确。






在这种特定情况下的简单解决方案:



要按原样引用属性值 ,只需将属性的 name 用作计算出的属性(列格式哈希表)中的表达式 条目。换句话说:在这种情况下,不需要包含表达式 [scriptblock] 实例( {... } ),只有一个 [string] 值包含属性 name



因此,以下内容也可以使用:

 #使用属性* name *作为'Expression '条目的值。 
$ format + = @ {标签= $ p;表达式= $ p}

请注意,这种方法恰好是避免问题,因为 $ p 在赋值时被评估为 ,因此捕获了循环迭代特定的值。






[1]重现: function foo {param([ValidateNotNull()] $ bar){} .GetNewClosure()};当调用 .GetNewClosure()时,foo 失败,错误为 Exception,调用带有 0参数的 GetNewClosure ):无法添加该属性,因为带有值的变量bar不再有效。

也就是说,尝试包含 unbound -bar 参数值- $ bar 变量-在闭包中,显然默认为 $ null ,这违反了其验证属性。

传递有效的 -bar 值可以使问题消失;例如, foo -bar''

将其视为 bug 的理由:如果函数本身在没有 -bar 参数值的情况下将 $ bar 视为不存在,因此 .GetNewClosure()


$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

In the above example, I want to access each object's property through a variable name. But it cannot work as expected. So in my case, how to make

Expression = {$_.$p}

working?

解决方案

The OP's code and this answer use PSv3+ syntax. Casting a hashtable to [pscustomobject] is not supported in PSv2, but you can replace [pscustomobject] $_ with New-Object PSCustomObject -Property $_.

As in many cases in the past, PetSerAl has provided the answer in terse (but very helpful) comments on the question; let me elaborate:

Your problem is not that you're using a variable ($p) to access a property per se, which does work (e.g., $p = 'Year'; Get-Date | % { $_.$p }).

Instead, the problem is that $p in script block { $_.$p } isn't evaluated until later, in the context of the Format-Table call, which means that the same, fixed value is used for all input objects - namely the value of $p at that point (which happens to be the last value that was assigned to $p in the foreach loop).

The cleanest and most generic solution is to call .GetNewClosure() on the script block to bind $p in the script block to the then-current, loop-iteration-specific value.

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

From the docs (emphasis added):

In this case, the new script block is closed over the local variables in the scope that the closure is defined in. In other words, the current values of the local variables are captured and enclosed inside the script block that is bound to the module.

Note that automatic variable $_ is undefined inside the foreach loop (PowerShell defines it only in certain contexts as the input object at hand, such as in script blocks passed to cmdlets in a pipeline), so it remains unbound, as desired.

Caveats:

  • While .GetNewClosure() as used above is convenient, it has the inefficiency drawback of invariably capturing all local variables, not just the one(s) needed.

  • A more efficient alternative that avoids this problem - and notably also avoids a bug (as of Windows PowerShell v5.1.14393.693 and PowerShell Core v6.0.0-alpha.15) in which the closure over the local variables can break, namely when the enclosing script / function has a parameter with validation attributes such as [ValidateNotNull()] and that parameter is not bound (no value is passed)[1] - is the following, significantly more complex expression Tip of the hat again to PetSerAl, and Burt_Harris's answer here :

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

    • & { ... } creates a child scope with its own local variables.
    • $p = $p then creates a local $p variable from its inherited value.
      To generalize this approach, you must include such a statement for each variable referenced in the script block.
    • { $_.$p }.GetNewClosure() then outputs a script block that closes over the child scope's local variables (just $p in this case).
    • The bug has been reported as an issue in the PowerShell Core GitHub repository and has since been fixed - it's unclear to me in what versions the fix will ship.
  • For simple cases, mjolinor's answer may do: it indirectly creates a script block via an expanded string that incorporates the then-current $p value literally, but note that the approach is tricky to generalize, because just stringifying a variable value doesn't generally guarantee that it works as part of PowerShell source code (which the expanded string must evaluate to in order to be converted to a script block).

To put it all together:

# 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

This yields:

ID Name Age
-- ---- ---
1  abc  3  
2  def  4  

as desired: the output columns use the column labels specified in $properties while containing the correct values.

Note how I've removed unnecessary ; instances and replaced built-in aliases % and ft with the underlying cmdlet names for clarity. I've also assigned distinct age values to better demonstrate that the output is correct.


Simpler solution, in this specific case:

To reference a property value as-is, without transformation, it is sufficient to use the name of the property as the Expression entry in the calculated property (column-formatting hashtable). In other words: you do not need a [scriptblock] instance containing an expression in this case ({ ... }), only a [string] value containing the property name.

Therefore, the following would have worked too:

# Use the property *name* as the 'Expression' entry's value.
$format += @{ Label = $p; Expression = $p }

Note that this approach happens to avoid the original problem, because $p is evaluated at the time of assignment, so the loop-iteration-specific values are captured.


[1] To reproduce: function foo { param([ValidateNotNull()] $bar) {}.GetNewClosure() }; foo fails when .GetNewClosure() is called, with error Exception calling "GetNewClosure" with "0" argument(s): "The attribute cannot be added because variable bar with value would no longer be valid."
That is, an attempt is made to include the unbound -bar parameter value - the $bar variable - in the closure, which apparently then defaults to $null, which violates its validation attribute.
Passing a valid -bar value makes the problem go away; e.g., foo -bar ''.
The rationale for considering this a bug: If the function itself treats $bar in the absence of a -bar parameter value as nonexistent, so should .GetNewClosure().

这篇关于Powershell:使用变量在脚本块中引用$ _的属性的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆