【问题标题】:How *exactly* does the RHS of PowerShell's -f operator work?PowerShell 的 -f 运算符的 RHS *究竟*如何工作?
【发布时间】:2010-12-24 17:39:12
【问题描述】:

Last time I got confused 顺便说一句 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 那样优雅地降级?

【问题讨论】:

    标签: arrays powershell expression operator-precedence


    【解决方案1】:

    嗯,这肯定有一个错误。 (实际上,我昨天刚刚写了a page on the PoshCode Wiki 关于它,还有一个bug on connect)。

    先回答,后面有更多问题:

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

    <##> $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 那样优雅地降级?

    这基本上是相同的错误,但表现形式不同。除非您手动调用它们的原生 .ToString() 方法,或者直接将它们传递给 String.Format() ,否则数组不应该在 PowerShell 中以“System.Object[]”的形式出现……它们在 #4 中所做的原因是那个错误:在将它们传递给 String.Format 调用之前,PowerShell 未能将它们扩展为 PSOjbect。

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


    但我想知道,考虑到我的第一个示例,您喜欢哪种行为以及您认为哪种行为正确?我认为 $OFS 分隔的输出是正确的,而不是像 @(wrap) 它那样展开数组,或者将其转换为 [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[]
    

    我确信很多脚本是在不知不觉中利用这个错误编写的,但我仍然很清楚 只是为了清楚示例中发生的展开不是正确的行为,但正在发生,因为在调用 .Net 时(在 PowerShell 的核心内)String.Format( "{0}", a ) ...$aobject[],这是 String.Format 所期望的,因为它是 Params 参数...

    我认为必须解决这个问题。如果想要保持展开数组的“功能”,应该使用@splatting 操作符来完成,对吧?

    【讨论】:

    • 很棒的总结 - 即使我不同意你的意见,也标记为答案。我认为 "{0} {1}" -f "a", "b" 应该相当于 $arr="a","b"; “{0} {1}”-f $arr。也就是说,-f 运算符的 RHS 应该是 [params object[]],以混合 C# 和 PS 术语。我不明白为什么应该使用 OFS 将 $arr 转换为字符串,除非您在运算符的 RHS 上明确引用它。
    • 嗯,它应该被转换为字符串的主要原因是字符串格式化应该做的。也就是说,如果您不使用像 { "{0:X}" -f 42 } 这样的格式化代码,那么字符串格式化的行为应该类似于将事物转换为字符串。这就是 .Net 的字符串格式化的工作方式。更改 PowerShell 的规则令人困惑。当然,他们已经用 $OFS 对其进行了更改……但是再次更改它意味着数组将输出至少三种不同的方式,而您永远不知道是哪种方式。例如,如果我这样做会发生什么:{ "{0}{1}" -f $arr,"hello" }?
    • 老实说,我的一部分同意你的观点,因为这样做很方便!但我希望它只明确发生,所以我可以同时做这两个:{ $a = 1,"+",2; "{0} = {1}" -f $a,"three"; "{2}-{0}={3}" -f @a,"one" } 并让它们出现:“1 + 2 = 三”和“2-1 = 一”......现在你可以做到这一点,但前提是你像狐狸一样疯狂:{ $a = 1,"+",2; "{0} = {1}" -f [PSObject]$a,"three"; "{2}-{0}={3}" -f [object[]]($a+"one") } 而那是不可知的。
    • 实际上,这也可以:[PSObject]$a = 1,"+",2; "{0} = {1}" -f $a,"three"; "{2}-{0}={3}" -f @($a+"one") 但它只是稍微不那么神秘......
    • 我会满足于此,只要您可以轻松切换模式(例如添加 splat 运算符)。只要他们给我们一些一致性和真实的文档! :)
    【解决方案2】:

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

    案例 9 和 10 似乎表明 PowerShell 中存在错误。拿这个修改后的 sn-p 试试看:

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

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

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

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

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

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

    【讨论】:

    • 酷。我将提交一个 Connect 错误。
    猜你喜欢
    • 2020-05-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-07
    • 2021-06-19
    • 1970-01-01
    相关资源
    最近更新 更多