1

This Q&A established that the powershell pipeline unrolls some collections sometimes. Suppose we have a function that emits a collection that might get unrolled, but we don't want the pipeline to do any unrolling. Here is an example demonstrating the unwanted unrolling:

Function EmitStack{
    [Cmdletbinding()]
    param()
    process{[System.Collections.Stack]@(10,20,30)}
}

$stack = [System.Collections.Stack]@(10,20,30)
$stack.GetType()
$EmittedStack = EmitStack
$EmittedStack.GetType()

#Name      BaseType                                                                                
#----      --------
#Stack     System.Object
#Object[]  System.Array

What we want is the $EmittedStack variable to contain the unadulterated, untouched [System.Collections.Stack] object that the function puts in the pipeline. Instead, it contains the unrolled items of the stack, re-rolled into an array. Is there a way for the caller to get the original object without subjecting it to powershell's pipeline unrolling (and re-rolling)?


Clarification: This question is about whether the caller can get the unadulterated object even if it hasn't been wrapped in a sacrificial array by the function before it puts it in the pipeline. In other words, I'm looking for a solution that doesn't involve changing the function.

Reason: Which types are unrolled is not clearly defined in powershell which implies a higher degree of unpredictability from system to system, powershell-version to powershell-version, dotnet-version to dotnet-version, etc. In case, for example, some powershell environments' pipelines unexpectedly unroll [System.Collections.Generic.Dictionary] (remember there are no documented rules for this) it would help for the caller to be able to compensate as needed without having to dig into the callee code.

Community
  • 1
  • 1
alx9r
  • 3,675
  • 4
  • 26
  • 55
  • I'm not sure I understand your concern. Usually unrolling is beneficial. How would a function not know that it will return something that will be subject to unwanted unrolling? In other words, why would it be unacceptable to change the function? Both caller and callee would know they are expecting one single object, that the caller is then free to unroll again, should they wish. – Jeroen Mostert Feb 25 '15 at 17:07
  • It's only a problem if both of the following conditions are true: 1. The powershell environment has changed such that the pipeline now unrolls types that it previously did not unroll. 2. That type is emitted by an opaque source like a third-party or a Microsoft built-in library. – alx9r Feb 25 '15 at 17:14
  • Powershell is a scripting language, and those emphasize productivity over robustness. If you want robust results, make sure your scripts run in a perfectly stable environment. You can force a particular version of Powershell with the `-version` switch, which should be enough to prevent this sort of thing from happening unexpectedly. Of course, that means testing scripts in the new version could be a chore, but that's not a common scenario (if it ain't broke, don't upgrade it). – Jeroen Mostert Feb 25 '15 at 17:23
  • Yup. That's becoming clear. My strategy is evolving to include way more automated tests where I need robustness. – alx9r Feb 25 '15 at 17:32

1 Answers1

2

Powershell only unrolls once. The standard technique therefore is to capture the result in a one-item array, which will get unrolled and give you back the original object:

Function EmitStack{
    [Cmdletbinding()]
    param()
    process{(,[System.Collections.Stack]@(10,20,30))}
}

Note the (,item) trick.

Edit: there is no way for a caller to get the original object after unrolling -- Powershell doesn't maintain some sort of special pseudo-variable that contains the content before it was unrolled. After unrolling, it would be indistinguishable from a regular enumerable. Although you could devise solutions that bypass the pipeline altogether (pass a parameters and assign a property, or maybe get very creative with a closure) changing the function would be unavoidable.

As @Matt points out, if the caller knows what type it's supposed to get back (or rather, if the caller wants to ensure it's always a specific type) they could just always convert the result to their desired type, so it doesn't matter if the result is unrolled or not. This still doesn't get you back the original object if unrolling did take place (but probably a very serviceable copy), and it assumes that Powershell can always convert to a type if it knows how to unroll objects of that type, which is probably not universally true.

Jeroen Mostert
  • 27,176
  • 2
  • 52
  • 85
  • Of course. But my questions is whether the _caller_ can get the original object if the function doesn't use the leading comma to wrap it in a sacrificial array. – alx9r Feb 25 '15 at 16:46
  • Once it's unrolled, there's no going back. The sender must take precautions to avoid unintentional unrolling. – Ansgar Wiechers Feb 25 '15 at 16:48
  • Ugh. This poorly defined pipeline unrolling is shaping up to be a nasty source of undefined behavior. – alx9r Feb 25 '15 at 17:05
  • You could strongly cast the return to ensure `$EmittedStack = [System.Collections.Stack](EmitStack)` @alx9r – Matt Feb 25 '15 at 17:21
  • @Matt So the object would go through these transformations: Original object=>unrolled items in pipeline=>rerolled items in array=>original type created from rerolled array. I wonder whether side-effects would be a problem. – alx9r Feb 25 '15 at 17:26
  • After looking at `Trace-Command` for this it would seem you are right. @alx9r. `Converting "System.Object[]" to "System.Collections.Stack".` – Matt Feb 25 '15 at 17:42