1

I am writing a code where it gets all files matching a criteria. For each file found, I create a custom object with only few of the file's ($Obj) values and then add this to an ArrayList ($Files). This ArrayList is then returned to the invoking function and stored in the variable of it ($OldFiles).

This works fine when no files or more than one are found. However, if only one file is found, the ArrayList converts itself to an PSCustomObject and then it throws an exception as the return cannot convert PSCustomObject to ArrayList.

The curious part is, if I add the simple line "$Files.getType()" before the return statement, there is no error and the ArrayList remains as an ArrayList.

Basically, this will not work: Simplified in the first function:

$Files = [System.Collections.ArrayList]@()
$Files.Add($Obj) > $null
return $Files

And this is how it looks in the top function:

$OldFiles = [System.Collections.ArrayList](Check-OldFiles -Path $Directory -Age $Age -FilesOnly:$FilesOnly -Recurse:$Recurse -UserCreationTime:$UserCreationTime)

But this works fine: Simplified in the first function:

$Files = [System.Collections.ArrayList]@()
$Files.Add($Obj) > $null
$Files.getType()
return $Files

And this is how it looks in the top function:

$OldFiles = [System.Collections.ArrayList](Check-OldFiles -Path $Directory -Age $Age -FilesOnly:$FilesOnly -Recurse:$Recurse -UserCreationTime:$UserCreationTime)

Why could this possibly be happening?

PowerShell version is 5.0

2
  • 2
    powershell unrolls a collection when sending it across the pipeline OR when returning it from a function. that means it sends it out one-at-a-time ... that also means a one-item collection will be just one item when it arrives at the other end of the call. you can use a leading comma ,$Collection to wrap the collection in an array before you send it out ... that will unwrap as a collection. Commented Dec 30, 2019 at 10:26
  • @Lee_Dailey I was aware of the unpacking when using the pipeline but I wasn't aware that it did the same when returning from a Function. Your hint of using the comma before the Collection worked like a charm, thanks a lot! Commented Dec 30, 2019 at 11:49

2 Answers 2

4

Clarification, caution (both @Bender the Greatest) and explanation (@Phragos):

  1. @() does not "wrap" its content. It converts its content to an Array. If its content is already an Array then it returns it as is. If it is a value or a single object then it copies it into a one element Object[]. If it is some other form of collection (e.g. ArrayList) then it copies each element into an Object[]. (Note: "@()" is an empty Object[].)

  2. Don't use ,(...) to force ... to be an Array. The automatic unwrapping behaviour when outputting objects (display, file, printer, etc...) can be misleading.

    $myCollection = ,(get-process)
    

    does not leave $myCollection with an Array of processes unless there is only one process (such as from get-process -name). Instead, $myCollection will be an Array of a single element containing the whole output (either a single object or an Array of the output objects). This fact is obscured by the format output cmdlets (Format-Table, Format-List and Format-Wide) which examine the objects received and, if any are Arrays, unwrap them. (More detail How PowerShell Formatting and Outputting REALLY works).

    You say "But if I do this it works fine":

    $myCollection | foreach { $_.starttime }
    

    and I say, yes, you get a list of starttimes BUT not the way you think. In the above, $myCollection is unwrapped and each element is sent into the pipeline to foreach. There is only one element which is an Array so the foreach loops once with $_ set to an Array of processes. Since Array is a collection but has no starttime member, PowerShell (V3+) performs a member enumeration creating an Array consisting of the values of the starttime members of each element of $_. This Array is then unwrapped and sent into the pipeline.

    You say "So what, the effect is the same" and I reply, yes until you try something more complicated:

    $myFiles = ,(gci -file *)
    $myFiles | foreach { $_.name+' '+$_.length }
    

    Expected output is a list of file names and lengths. Actual output is a list of names, a blank line and the number of files. Why? $myFiles is an Array of one element containing the list of files so foreach loops once with $_ being this list. An Array has no name member so PowerShell performs a member enumeration producing an Array of names. Next, a String of one space is added (concatenated) to this Array. Finally, $_.length is added. Since an Array does have a length member, this value (an Int32) is added to the Array of names and ' '. This Array is then unwrapped and sent out.

    Summary, if you want something that might or might not be an Array to definitely be an Array, don't use ,(...). Always use @(...), that's what it's for.

    However, in other situations, the use of ,(...) is appropriate but can still create confusion.

    When parsing the command line, spaces separate tokens not arguments. Therefore, the following are equivalent:

    1,2,3
    
    1  , 2,      3
    

    This can trip you up when using the prepended , to create an array of one item. Say you have a cmdlet that takes an Array as a parameter but you want to supply only one element that itself is an Array. For example,

    new-object collections.arraylist ,(1,2,3)
    New-Object : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'ComObject'. Specified method is not supported.
    At line:1 char:12
    + new-object collections.arraylist ,(1,2,3)
    +            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [New-Object], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.PowerShell.Commands.NewObjectCommand
    

    Since the space before the , was essentially ignored by the parser (, is a single character token so doesn't need separation), this was parsed as:

     new-object ("collections.arraylist",(1,2,3))
    

    so in this case it should be:

     new-object collections.arraylist (,(1,2,3))
    

    Note: @(1,2,3) would not work since 1,2,3 is already an Array.

  3. The return statement in Powershell is not like in functional languages in that it is not the only way to produce output. Every expression that produces a value other than void is sent into the pipeline, i.e. collected for return (values which are arrays are unrolled and the elements collected individually). return causes the function to return after the value is generated (it does not "return" the value). I am assuming that there are no values being generated by the the unseen parts of the function. However, putting $Files.gettype() into the function causes a value to be generated (a System.RuntimeType). Therefore, the function always returns 1 more object than before. I have to assume that the case of 0 matching files was not tested (the function already works for this case, right?) because now we are back to one object so the function returns a "value" again (System.RuntimeType). I don't know how the returned objects are being used but there is no mention of problems caused by the first element of the ArrayList being a System.RuntimeType instead of a PSCustomObject. Presumably, the usage did not mind getting $null for the properties which don't exist (only [String]Name and [String]Fullname are common to both System.RuntimeType and System.IO.FileInfo) and that Attributes caused no type conflicts (System.RuntimeType.Attributes and System.IO.FileInfo.Attributes are both System.Enum but the value of [Collections.ArrayList].Attributes is not a valid IO.FileInfo.Attributes value, or so PowerShell says).

† Assignment statements are of type void while assignment expressions (assignments within an expression) have non-void type. Thus,

$a = 5;       // void
($a = 5)      // int
($a = 5);     // also int
if ($a = 5){} // valid
// the assignment is evaluated as part of a conditional expression (value 5 -> $true)
Sign up to request clarification or add additional context in comments.

Comments

1

Collections are automatically unrolled in PowerShell as the individual elements are passed down the pipeline. If you want to guarantee that a variable remains a collection even if there is only one element, there are a few tricks you can use (the examples below use Get-Process but any collection variable or cmdlet that returns a collection can be used):

  1. Wrap your command in an array (e.g. $myCollection = @(Get-Process))
  2. Prefix your potential collection with a comma , character (e.g. $myCollection = , (Get-Process)

The @() specified an array, and you can either provide it a list of elements or use a cmdlet that may return a collection.

The , (Get-Process) syntax looks odd, but this is the same syntax as specifying a hardcoded list of elements for an array. Consider the following:

$myArray = 1, 2, 3, 4, 5, "Bender"

This creates an array with the elements 1, 2, 3, 4, 5, and "Bender". In the shorthand above though, merely providing the , as the first character in the expression signifies that this is an array, treat it as such. Due to the nature of PowerShell unrolling collections, any subsequent arrays/collections that are returned after that initial comma will be added to the final collection.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.