How * exactly does the RHS of the PowerShell -f operator work?

The last time I got confused , PowerShell was looking forward to expanding its collections, Keith summed up his heuristic like this:

Putting the results (array) in a grouping expression (or a subexpression, such as $ ()) makes it suitable for expanding again.

I took this advice to heart, but still can not explain a few esoterics. In particular, the Format statement does not work 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 that we saw in the previous question, it is clear why cases like # 1 and # 5 behave differently: the pipeline operator signals the script engine to expand to another level, and the assignment operator does not. In other words, everything that lies between the two | treated as a grouped expression, as if it were 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 

For the same reason, case No. 7 does not improve compared to C # 5. Any attempt to add additional Wrap will be immediately undermined by the additional channel. Same # 8 vs # 6. A little disappointing, but I'm completely on board until this point.

Other questions:

  • Why is case No. 3 not suffering the same fate as # 4? $ rhs should contain a nested array (, ("a", "a")) , but its outer level expands ... somewhere ...
  • What happens to the different grouping operators in # 9-10? Why do they behave so randomly and why are they even needed?
  • Why are failures in the case of the 10th degradation graceful as # 4?
+3
arrays expression operator-precedence powershell
Dec 09 '09 at 7:16
source share
2 answers

Well, there is a mistake. (I just wrote a page on the PoshCode Wiki about this yesterday, and there’s a connection error ).

Answers first, more questions later:

To get the consistent behavior of arrays with -f string formatting, you will need to make 100% sure that they are PSObjects objects. My suggestion is to do this when they are appointed. It is assumed that this will be done automatically using PowerShell, but for some reason this will not be done until you get access to the property or something (as described in this wiki page and the error ). For example, ( <##> is my invitation):

 <##> $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 do this, they will NOT be expanded when passed to -f, but rather will be displayed correctly - just as they would if you put the variable in a string directly.

Why is case No. 3 not suffering the same fate as # 4? $ rhs should contain a nested array (, ("a", "a")), but its outer level expands ... somewhere ...

A simple version of the answer is that BOTH # 3 and # 4 are deployed. The difference is that in 4 the internal content is an array (even after the external array has been reversed):

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

What happens to the different grouping operators in # 9-10? Why do they behave so randomly and why are they even needed?

As I said before, an array should be considered the only parameter in the format and should be output using PowerShell string formatting rules (i.e. separated by $OFS ) just as if you entered $ _ into the string directly ... so when PowerShell behaves correctly, $lhs -f $rhs will fail if $ lhs contains two-local holders.

Of course, we have already noticed that there is an error in it.

I don't see anything messy: @ () and $ () work the same for 9 and 10, as far as I can see (the main difference, in fact, is related to how ForEach expands the top-level array:

 > $rhs = "a", "b" | SquareAndWrap > $rhs | % { $lhs -f @($_); " hi " } aa hi bb hi > $rhs | % { $lhs -f $($_); " hi " } aa hi bb hi # Is the same as: > [String]::Format( "{0} {1}", $rhs[0] ); " hi " aa hi > [String]::Format( "{0} {1}", $rhs[1] ); " hi " bb hi 

So, you see the error: @ () or $ () will cause the array to be passed as [object []] to the string format call instead of PSObject, which has special values ​​in the string.

Why are failures in the case of the 10th degradation graceful as # 4?

This is basically the same mistake, in a different manifestation. Arrays should never appear as "System.Object []" in PowerShell unless you manually name your own .ToString () method or pass them directly to String.Format () ... the reason they do in # 4 is an error: PowerShell was unable to extend them as PSOjbects before passing them to a String.Format call.

This can be seen if you accessed the property of the array before passing it, or passed it to PSObject, as in my original exams. Technically, errors in # 10 are the correct conclusion: you only pass ONE thing (array) to string.format when it expected TWO. If you change $ lhs to "{0}", you will see an array formatted with $ OFS




It is interesting, however, what kind of behavior are you like and which, in your opinion, is correct , given my first example? I think that the output signal of $ OFS is correct, as opposed to expanding the array, as it happens if you @ (wrap) it or pass it [object []] (by the way, note what happens if you add it to [ int []] is another error 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 that many scripts were written unconsciously using this error, but it still seems clear to me that the deployment that happens with a simple example is NOT the right behavior, but because, on a call (inside the PowerShell core), .Net String.Format( "{0}", a ) ... $a is object[] , which is what String.Format is supposed to be like as a parameter to Params ...

I think this should be fixed. If there is a desire to preserve the "functionality" of array expansion, this should be done using the @splatting operator, right?

+5
Dec 09 '09 at 20:46
source share

Neither square nor Wrap will do what you are trying in # 5 and 7. Regardless of whether you put the array in the 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 expanded, because it is fed to the next stage of the pipeline one at a time. Similarly in 6 and 8, it doesn’t matter that you pipe in several objects, both Square and Wrap will feed them 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:

 "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 foreach alreadyd gets an object [] size 2, so $_ should work without casting [object []] or wrapping in a subexpression of a subexpression or array. We have seen some V2 errors related to improper unpacking of psobject, and this seems to be another example of this. If you deploy psobject manually, this works, for example. $_.psobject.baseobject .

I think you are shooting Wrap, this is:

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

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

By the way, parens in Square and Wrap, and external parens in SquareAndWrap are not needed.

+2
Dec 09 '09 at 16:32
source share



All Articles