PowerShell 的 -f 运算符的 RHS“到底”是如何工作的?

发布于 2024-08-14 05:40:55 字数 2626 浏览 8 评论 0原文

顺便说一下,上次我感到困惑 PowerShell 急切地展开集合,Keith 总结其启发式如下:

将结果(数组)放入分组表达式(或子表达式,例如 $())中,使其再次符合展开条件。

我已经牢记了这个建议,但仍然发现自己无法解释一些深奥的知识。特别是,Format 运算符似乎不遵守规则。

$lhs = "{0} {1}"

filter Identity { $_ }
filter Square { ($_, $_) }
filter Wrap { (,$_) }
filter SquareAndWrap { (,($_, $_)) }

$rhs = "a" | Square        
# 1. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | Square | Wrap       
# 2. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | SquareAndWrap       
# 3. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a", "b" | SquareAndWrap       
# 4. all succeed by coercing the inner array to the string "System.Object[]"
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

"a" | Square | % {
    # 5. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | % {
    # 6. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | Square | Wrap | % {
    # 7. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | Wrap | % {
    # 8. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | SquareAndWrap | % {
    # 9. only @() and $() succeed
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | SquareAndWrap | % {
    # 10. only $() succeeds
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

应用我们在上一个问题中看到的相同模式,很明显为什么像 #1 和 #5 这样的情况表现不同:管道运算符向脚本引擎发出信号以展开另一个级别,而赋值运算符则不然。换句话说,两个 | 之间的所有内容都被视为分组表达式,就像它在 () 内部一样。

# all of these output 2
("a" | Square).count                       # explicitly grouped
("a" | Square | measure).count             # grouped by pipes
("a" | Square | Identity).count            # pipe + ()
("a" | Square | Identity | measure).count  # pipe + pipe

出于同样的原因,案例 #7 并没有比 #5 有所改进。任何添加额外Wrap的尝试都会立即被额外的管道破坏。同上 #8 与 #6。有点令人沮丧,但到目前为止我完全同意。

剩下的问题:

  • 为什么案例#3 没有遭受与#4 相同的命运? $rhs 应该保存嵌套数组 (,("a", "a")) 但它的外层正在展开......某处......
  • 发生了什么事与 #9-10 中的各种分组运算符?为什么他们的行为如此不稳定,为什么需要他们?
  • 为什么案例 #10 中的失败不会像案例 #4 那样优雅地降级?

Last time I got confused by the way PowerShell eagerly unrolls collections, Keith summarized its heuristic like so:

Putting the results (an array) within a grouping expression (or subexpression e.g. $()) makes it eligible again for unrolling.

I've taken that advice to heart, but still find myself unable to explain a few esoterica. In particular, the Format operator doesn't seem to play by the rules.

$lhs = "{0} {1}"

filter Identity { $_ }
filter Square { ($_, $_) }
filter Wrap { (,$_) }
filter SquareAndWrap { (,($_, $_)) }

$rhs = "a" | Square        
# 1. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | Square | Wrap       
# 2. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a" | SquareAndWrap       
# 3. all succeed
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

$rhs = "a", "b" | SquareAndWrap       
# 4. all succeed by coercing the inner array to the string "System.Object[]"
$lhs -f $rhs
$lhs -f ($rhs)
$lhs -f $($rhs)
$lhs -f @($rhs)

"a" | Square | % {
    # 5. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | % {
    # 6. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | Square | Wrap | % {
    # 7. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | Square | Wrap | % {
    # 8. all fail
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a" | SquareAndWrap | % {
    # 9. only @() and $() succeed
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

"a", "b" | SquareAndWrap | % {
    # 10. only $() succeeds
    $lhs -f $_
    $lhs -f ($_)
    $lhs -f @($_)
    $lhs -f $($_)            
}

Applying the same patterns we saw in the previous question, it's clear why cases like #1 and #5 behave different: the pipeline operator signals the script engine to unroll another level, while the assignment operator does not. Put another way, everything that lies between two |'s is treated as a grouped expression, just as if it were inside ()'s.

# all of these output 2
("a" | Square).count                       # explicitly grouped
("a" | Square | measure).count             # grouped by pipes
("a" | Square | Identity).count            # pipe + ()
("a" | Square | Identity | measure).count  # pipe + pipe

For the same reason, case #7 is no improvement over #5. Any attempt to add an extra Wrap will be immediately subverted by the extra pipe. Ditto #8 vs #6. A little frustrating, but I'm totally on board up to this point.

Remaining questions:

  • Why doesn't case #3 suffer the same fate as #4? $rhs should hold the nested array (,("a", "a")) but its outer level is getting unrolled...somewhere...
  • What's going on with the various grouping operators in #9-10? Why do they behave so erratically, and why are they needed at all?
  • Why don't the failures in case #10 degrade gracefully like #4 does?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

折戟 2024-08-21 05:40:55

嗯,这肯定有一个错误。 (实际上,我昨天刚刚在 PoshCode Wiki 上写了关于它的页面,并且有连接错误)。

首先回答,然后再回答更多问题:

要从具有 -f 字符串格式的数组获得一致的行为,您需要 100% 确定它们是 PSObject。我的建议是在分配它们时这样做。它应该由PowerShell自动完成,但由于某种原因,直到您访问属性或其他内容(如wiki 页面错误)。例如( <##> 是我的提示):

<##> $a = 1,2,3
<##> "$a"
1 2 3

<##> $OFS = "-"  # Set the Output field separator
<##> "$a"
1-2-3

<##> "{0}" -f $a
1 

<##> $a.Length
3 

<##> "{0}" -f $a
1-2-3

# You can enforce correct behavior by casting:
<##> [PSObject]$b = 1,2,3
<##> "{0}" -f $a
1-2-3

请注意,当您这样做时,它们在传递给 -f 时不会展开,而是会正确输出 - 就像它们的方式一样如果您直接将变量放入字符串中,则会出现这种情况。

为什么案例#3 没有遭受与#4 相同的命运? $rhs 应该保存嵌套数组 (,("a", "a")) 但它的外层正在展开...某处...

答案的简单版本是 #3 和 #4 都正在展开。不同之处在于,在 4 中,内部内容是一个数组(即使在展开外部数组之后):

$rhs = "a" | SquareAndWrap
$rhs[0].GetType()  # String

$rhs = "a","b" | SquareAndWrap
$rhs[0].GetType()  # Object[]

#9-10 中的各种分组运算符是怎么回事?为什么他们的行为如此不稳定,为什么需要他们?

正如我之前所说,数组应该算作格式的单个参数,并且应该使用 PowerShell 的字符串格式规则输出(即:用 $OFS 分隔)就像您那样直接将 $_ 放入字符串 ...因此,当 PowerShell 正常运行时,如果 $lhs 包含两个占位符,$lhs -f $rhs 将失败。

当然,我们已经观察到其中存在错误。

但是,我没有发现任何不稳定的情况:据我所知,@() 和 $() 对于 9 和 10 的工作方式相同可以看到(事实上,主要区别是由 ForEach 展开顶级数组的方式引起的:

> $rhs = "a", "b" | SquareAndWrap
> $rhs | % { $lhs -f @($_); " hi " }
a a
 hi 
b b
 hi 

> $rhs | % { $lhs -f $($_); " hi " }
a a
 hi 
b b
 hi     

# Is the same as:
> [String]::Format( "{0} {1}", $rhs[0] ); " hi "
a a
 hi 

> [String]::Format( "{0} {1}", $rhs[1] ); " hi "
b b
 hi     

所以你看到的错误是 @() 或 $() 会导致数组作为 [object[]] 传递到字符串格式调用而不是作为具有特殊 to-string 值的 PSObject,

为什么案例 #10 中的失败不会像 #4 那样优雅地降级

?在 PowerShell 中显示为“System.Object[]”,除非您手动调用其本机 .ToString() 方法,或直接将它们传递给 String.Format() ...他们在 # 中这样做的原因4 是那个错误:PowerShell 在将它们传递给 String.Format 调用之前未能将它们扩展为 PSOjbects,

如果您在传递数组的属性之前访问它,或者将其转换为 PSObject(如我的原始示例中所示),您可以看到这一点。 。从技术上讲,#10 中的错误是正确的输出:您只将一个东西(数组)传递给 string.format,而它需要两个东西。如果您将 $lhs 更改为“{0}”,您会看到用 $OFS 格式化的数组,


但我想知道您喜欢哪种行为以及您认为哪种行为正确不同的错误行为):

> "{0}" -f [object[]]$a
1

> "{0}, {1}" -f [object[]]$a  # just to be clear...
1,2

>  "{0}, {1}" -f [object[]]$a, "two"  # to demonstrate inconsistency
System.Object[],two

> "{0}" -f [int[]]$a
System.Int32[]

我确信很多脚本都是在不知不觉中利用这个错误编写的,但对我来说,仍然很清楚中发生的展开>需要明确的是示例不是正确的行为,但之所以发生,是因为在调用(在PowerShell的核心内部).Net String.Format( "{0}", a ) ... $a 是一个 object[] ,这是 String.Format 所期望的,因为它是 Params 参数...

我认为必须修复它。如果想要保留展开数组的“功能”,应该使用 @ splatting 运算符来完成,对吧?

Well, there's a bug in that for sure. (I just wrote up a page on the PoshCode Wiki about it yesterday, actually, and there's a bug on connect).

Answers first, more questions later:

To get consistent behavior from arrays with the -f string formatting, you're going to need to make 100% sure they are PSObjects. My suggestion is to do that when assigning them. It is supposed to be done automatically by PowerShell, but for some reason isn't done until you access a property or something (as documented in that wiki page and bug). E.g.( <##> is my prompt):

<##> $a = 1,2,3
<##> "$a"
1 2 3

<##> $OFS = "-"  # Set the Output field separator
<##> "$a"
1-2-3

<##> "{0}" -f $a
1 

<##> $a.Length
3 

<##> "{0}" -f $a
1-2-3

# You can enforce correct behavior by casting:
<##> [PSObject]$b = 1,2,3
<##> "{0}" -f $a
1-2-3

Note that when you've done that, they WILL NOT be unrolled when passing to -f but rather would be output correctly -- the way they would be if you placed the variable in the string directly.

Why doesn't case #3 suffer the same fate as #4? $rhs should hold the nested array (,("a", "a")) but its outer level is getting unrolled...somewhere...

The simple version of the answer is that BOTH #3 and #4 are getting unrolled. The difference is that in 4, the inner contents are an array (even after the outer array is unrolled):

$rhs = "a" | SquareAndWrap
$rhs[0].GetType()  # String

$rhs = "a","b" | SquareAndWrap
$rhs[0].GetType()  # Object[]

What's going on with the various grouping operators in #9-10? Why do they behave so erratically, and why are they needed at all?

As I said earlier, an array should count as a single parameter to the format and should be output using PowerShell's string-formatting rules (ie: separated by $OFS) just as it would if you put $_ into the string directly ... therefore, when PowerShell is behaving correctly, $lhs -f $rhs will fail if $lhs contains two place holders.

Of course, we've already observed that there's a bug in it.

I don't see anything erratic, however: @() and $() work the same for 9 and 10 as far as I can see (the main difference, in fact, is caused by the way the ForEach unrolls the top level array:

> $rhs = "a", "b" | SquareAndWrap
> $rhs | % { $lhs -f @($_); " hi " }
a a
 hi 
b b
 hi 

> $rhs | % { $lhs -f $($_); " hi " }
a a
 hi 
b b
 hi     

# Is the same as:
> [String]::Format( "{0} {1}", $rhs[0] ); " hi "
a a
 hi 

> [String]::Format( "{0} {1}", $rhs[1] ); " hi "
b b
 hi     

So you see the bug is that @() or $() will cause the array to be passed as [object[]] to the string format call instead of as a PSObject which has special to-string values.

Why don't the failures in case #10 degrade gracefully like #4 does?

This is basically the same bug, in a different manifestation. Arrays should never come out as "System.Object[]" in PowerShell unless you manually call their native .ToString() method, or pass them to String.Format() directly ... the reason they do in #4 is that bug:PowerShell has failed to extend them as PSOjbects before passing them to the String.Format call.

You can see this if you access a property of the array before passing it in, or cast it to PSObject as in my original exampels. Technically, the errors in #10 are the correct output: you're only passing ONE thing (an array) to string.format, when it expected TWO things. If you changed your $lhs to just "{0}" you would see the array formatted with $OFS


I wonder though, which behavior do you like and which do you think is correct, considering my first example? I think the $OFS-separated output is correct, as opposed to unrolling the array as happens if you @(wrap) it, or cast it to [object[]] (Incidentally, note what happens if you cast it to [int[]] is a different buggy behavior):

> "{0}" -f [object[]]$a
1

> "{0}, {1}" -f [object[]]$a  # just to be clear...
1,2

>  "{0}, {1}" -f [object[]]$a, "two"  # to demonstrate inconsistency
System.Object[],two

> "{0}" -f [int[]]$a
System.Int32[]

I'm sure lots of scripts have been written unknowingly taking advantage of this bug, but it still seems pretty clear to me that the unrolling that's happening in the just to be clear example is NOT the correct behavior, but is happening because, on the call (inside PowerShell's core) to the .Net String.Format( "{0}", a ) ... $a is an object[] which is what String.Format expected as it's Params parameter...

I think that has to be fixed. If there's any desire to keep the "functionality" of unrolling the array it should be done using the @ splatting operator, right?

乖乖公主 2024-08-21 05:40:55

Square 和 Wrap 都不会执行您在 # 的 5 和 7 中尝试的操作。无论您是像在 Square 中那样将数组放在分组表达式 () 中,还是像在 Wrap 中那样使用逗号运算符,当您在管道中使用这些函数时,它们的输出会展开,因为它一次一个地馈送到下一个管道阶段。同样,在 6 和 8 中,您通过管道输入多个对象并不重要,Square 和 Wrap 都会一次将一个对象提供给您的 foreach 阶段。

案例 9 和 10 似乎表明 PowerShell 中存在错误。获取这个修改后的片段并尝试一下:

"a" | SquareAndWrap | % {    
    # 9. only @() and $() succeed  
    $_.GetType().FullName
    $_.Length
    $lhs -f [object[]]$_
    $lhs -f [object[]]($_)    
    $lhs -f @($_)   
    $lhs -f $($_)            
}

它有效。它还表明 foreach 已经接收到一个 object[] 大小 2,因此 $_ 应该可以工作,而无需转换为 [object[]] 或包装在子表达式或数组子表达式中。我们已经看到一些与 psobjects 未正确展开相关的 V2 错误,这似乎是另一个例子。如果您手动解开 psobject,它会起作用,例如 $_.psobject.baseobject

我“认为”您在 Wrap 中的目标是:

function Wrap2 { Begin {$coll = @();} Process {$coll += $_} End {,$coll} }

这将累积所有管道输入,然后将其输出为单个数组。这适用于情况 8,但您仍然需要在前两次使用 -f 运算符时转换为 [object[]]。

顺便说一句,Square 和 Wrap 中的括号以及 SquareAndWrap 中的外部括号都是不必要的。

Neither Square not Wrap will do what you're trying in #'s 5 and 7. Regardless of whether you put an array within a grouping expression () as you do in Square or you use the comma operator as you do in Wrap, when you use these functions in the pipeline their output is unrolled as it is feed to the next pipeline stage one at a time. Similarly in 6 and 8, it doesn't matter that you pipe in multiple objects, both Square and Wrap will feed them out one at a time to your foreach stage.

Cases 9 and 10 seem to indicate a bug in PowerShell. Take this modified snippet and try it:

"a" | SquareAndWrap | % {    
    # 9. only @() and $() succeed  
    $_.GetType().FullName
    $_.Length
    $lhs -f [object[]]$_
    $lhs -f [object[]]($_)    
    $lhs -f @($_)   
    $lhs -f $($_)            
}

It works. It also shows that the foreach alreadyd receives an object[] size 2 so $_ should work without casting to [object[]] or wrapping in a subexpression or array subexpression. We have seen some V2 bugs related to psobjects not unwrapping correctly and this appears to be another instance of that. If you unwrap the psobject manually it works e.g. $_.psobject.baseobject.

I "think" what you are shooting for in Wrap is this:

function Wrap2 { Begin {$coll = @();} Process {$coll += $_} End {,$coll} }

This will accumulate all pipeline input and then output it as a single array. This will work for case 8 but you still need to cast to [object[]] on the first two uses of the -f operator.

BTW the parens in both Square and Wrap and the outer parens in SquareAndWrap are unnecessary.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文