PowerShell的-f运算符的RHS如何*完全*?

Ric*_*erg 3 arrays powershell expression operator-precedence

上次我PowerShell急切展开收藏的方式感到困惑,Keith总结了它的启发式:

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

我已经把这个建议铭记于心,但仍然发现自己无法解释一些esoterica.特别是,格式运算符似乎不符合规则.

$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 $($_)            
}
Run Code Online (Sandbox Code Playgroud)

应用我们在前一个问题中看到的相同模式,很清楚为什么像#1和#5这样的情况表现不同:管道运算符指示脚本引擎展开另一个级别,而赋值运算符则不然.换句话说,位于两个|之间的所有内容都被视为一个分组表达式,就像它是在inside()中一样.

# 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
Run Code Online (Sandbox Code Playgroud)

出于同样的原因,案例#7对#5没有任何改进.任何添加额外Wrap的尝试都会被额外的管道立即破坏.同上#8 vs#6.有点令人沮丧,但我完全在这一点上.

剩下的问题:

  • 为什么情况#3不会遭遇与#4相同的命运? $ rhs应该保存嵌套数组(,("a","a"))但它的外层正在展开......某处...
  • #9-10中各种分组运营商的情况如何?为什么他们表现得如此不稳定,为什么他们需要呢?
  • 为什么#10中的失败不像#4那样优雅地降级?

Jay*_*kul 5

好吧,肯定有一个错误.(我昨天在PoshCode Wiki上写了一个关于它的页面,实际上,连接上有一个错误).

首先回答,后面会提出更多问题:

要使用-f字符串格式从数组中获得一致的行为,您需要100%确定它们是PSObjects.我的建议是在分配时这样做.它应该由PowerShell自动完成,但由于某些原因,在您访问属性或其他内容之前不会这样做(如该Wiki页面bug中所述).例如(<##>是我的提示):

<##> $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
Run Code Online (Sandbox Code Playgroud)

请注意,当您完成此操作时,它们不会在传递给-f时展开,而是会正确输出 - 如果您将变量直接放在字符串中的方式.

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

答案的简单版本是#3和#4正在展开.区别在于,在4中,内部内容是一个数组(即使在外部数组展开后):

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

$rhs = "a","b" | SquareAndWrap
$rhs[0].GetType()  # Object[]
Run Code Online (Sandbox Code Playgroud)

#9-10中各种分组运营商的情况如何?为什么他们表现得如此不稳定,为什么他们需要呢?

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

当然,我们已经观察到它有一个错误.

然而,我没有看到任何不稳定的东西:@()和$()对于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     
Run Code Online (Sandbox Code Playgroud)

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

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

这基本上是相同的错误,在不同的表现形式.除非你手动调用它们的本机.ToString()方法,或者直接将它们传递给String.Format(),否则数组永远不应该作为PowerShell中的"System.Object []"出现...他们在#4中做的原因就是那个bug:PowerShell失败了在将它们传递给String.Format调用之前将它们扩展为PSOjbects.

如果在传入数组之前访问数组的属性,或者像在原始示例中那样将其强制转换为PSObject,则可以看到这一点.从技术上讲,#10中的错误是正确的输出:当你预期两件事时,你只将一个东西(一个数组)传递给string.format.如果您将$ lhs更改为"{0}",您将看到使用$ OFS格式化的数组


我想知道,考虑到我的第一个例子,你喜欢哪种行为,哪种行为是正确的?我认为$ OFS分离的输出是正确的,而不是展开数组,如果你@(包装)它,或者把它转换为[object []](顺便说一句,注意如果你把它转换为[int [会发生什么] ]]是一种不同的越野行为):

> "{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[]
Run Code Online (Sandbox Code Playgroud)

我确信很多脚本都是在不知不觉中编写好利用这个bug的,但我似乎仍然很清楚,在明确的例子中发生的展开不是正确的行为,而是正在发生,因为,在调用(在PowerShell的核心内部)到.Net String.Format( "{0}", a )... $aobject[]String.Format所期望的,因为它是Params参数...

我认为必须修复.如果有任何希望保持展开数组的"功能",那么应该使用@splatting运算符完成,对吗?