20

I'm trying to write a PowerShell script to build a list of files, from several directories. After all directories have been added to the main list, I'd like to do the same processing on all files.

This is what I have:

$items = New-Object Collections.Generic.List[IO.FileInfo]

$loc1 = @(Get-ChildItem -Path "\\server\C$\Program Files (x86)\Data1\" -Recurse)
$loc2 = @(Get-ChildItem -Path "\\server\C$\Web\DataStorage\" -Recurse)

$items.Add($loc1) # This line fails (the next also fails)
$items.Add($loc2)

# Processing code is here

which fails with this error:

Cannot convert argument "0", with value: "System.Object[]", for "Add" to type "System.IO.FileInfo": "Cannot convert the "System.Object[]" va lue of type "System.Object[]" to type "System.IO.FileInfo"."

I am mostly interested in what is the correct approach for this type of situation. I realize that my code is a very C way of doing it -- if there is a more PowerShell way to acomplish the same task, I'm all for it. The key, is that the number of $loc#'s may change over time, so adding and removing one or two should be easy in the resulting code.

Peter Mortensen
  • 28,342
  • 21
  • 95
  • 123
Nate
  • 28,586
  • 21
  • 107
  • 177

5 Answers5

32

Not sure you need a generic list here. You can just use a PowerShell array e.g.:

$items  = @(Get-ChildItem '\\server\C$\Program Files (x86)\Data1\' -r)
$items += @(Get-ChildItem '\\server\C$\Web\DataStorage\' -r)

PowerShell arrays can be concatenated using +=.

Keith Hill
  • 173,872
  • 36
  • 316
  • 347
  • 4
    Perhaps the need for `@()` should be explained(?). – Peter Mortensen Jan 22 '16 at 14:11
  • Get-ChildItem does not output Array if the output contains only one item. If this was not PS, we would need to check type of first output, Array vs Object, then process it accordingly. In PS, `(@(@('s','d')))[0].GetType()` is String, meaning that intent to create Array containing only one Array is ignored. In the example if first line would produce only one item and @() was not used, $items will be an Object and trying to join Array to an Object would cause error on second line. @() on second line is not needed, as you can join Object to an Array as well as Array to an Array by the same `+=` – papo May 09 '18 at 15:34
  • @papo | `Get-ChildItem does not output Array if the output contains only one item` Nevertheless, it works (at least on my system, PS v5.1.14393.3471). I can do this with no problem: `$Files = @(gci -Filter "$BuildName-$SemanticVersion*.nupkg"); $Files += @(gci -Filter "Setup.exe"); $Files`. – InteXX Jun 13 '20 at 20:58
  • @InteXX sorry did not mention my comment was an answer to Peter's comment. Your example is similar to Keith, so it's how it should be, but if you'd omit @(), as Peter asked about, it might misbehave. PS's auto-type is nice, but might create issues. If you need an Array, be specific. Or you might end up with an error, or on different occasion with an array of array entry where it was not expected. I follow rule, if it's a script be very concise. – papo Jun 13 '20 at 23:16
  • @papo | Ah, OK. Gotcha now. That makes sense. Actually, I ended up going with Roman's version, from below. `$Files = @(gci -Path $ReleaseDirectory -Filter "$BuildName-$SemanticVersion*.nupkg"; gci -Path $ReleaseDirectory -Filter "Setup.exe" gci -Path $ReleaseDirectory -Filter "RELEASES"); $Files` Nice and concise and it works great. – InteXX Jun 14 '20 at 06:28
  • Works.But imagine if you have one file in first dir and multiple in another. Doing gci will for 1st create a IO.FileSystemInfo object, but in second an Array of IO.FileSystemInfo objects. The same command but different output types. Now in your command @() your Array will be exploded into individual items, resulting in one dimensional Array. That's what desired. But consider the conversion going on in between. I would use this on a command line, but would want to keep a script file clean of such background conversions. Such non transparent flow makes debugging of more complex scripts painful. – papo Jun 14 '20 at 07:02
  • @papo | Thanks, I'll keep that in mind for my more complex scripts. This is just a ten-liner for a DevOps build task. Simple stuff. (Observation: the issue you describe here also occurs with the `+=` approach.) – InteXX Jun 14 '20 at 07:22
29

From get-help get-childitem: -Path Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (.).

$items = get-childitem '\\server\C$\Program Files (x86)\Data1\','\\server\C$\Web\DataStorage\' -Recurse
mjolinor
  • 59,504
  • 6
  • 99
  • 125
6

Here is some perhaps even more PowerShell-ish way that does not need part concatenation or explicit adding items to the result at all:

# Collect the results by two or more calls of Get-ChildItem
# and perhaps do some other job (but avoid unwanted output!)
$result = .{

    # Output items
    Get-ChildItem C:\TEMP\_100715_103408 -Recurse

    # Some other job
    $x = 1 + 1

    # Output some more items
    Get-ChildItem C:\TEMP\_100715_110341 -Recurse

    #...
}

# Process the result items
$result

But the code inside the script block should be written slightly more carefully to avoid unwanted output mixed together with file system items.

EDIT: Alternatively, and perhaps more effectively, instead of .{ ... } we can use @( ... ) or $( ... ) where ... stands for the code containing several calls of Get-ChildItem.

Roman Kuzmin
  • 36,549
  • 9
  • 84
  • 108
5

Keith's answer is the PowerShell way: just use @(...)+@(...).

If you actually do want a typesafe List[IO.FileInfo], then you need to use AddRange, and cast the object array to a FileInfo array -- you also need to make sure you don't get any DirectoryInfo objects, or else you need to use IO.FileSystemInfo as your list type:

So, avoid directories:

$items = New-Object Collections.Generic.List[IO.FileInfo]
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Program Files (x86)\Data1\' -r | Where { -not $_.PSIsContainer } )) )
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Web\DataStorage\' -r | Where { -not $_.PSIsContainer } )) )

Or use FileSystemInfo (the common base class of FileInfo and DirectoryInfo):

$items = New-Object Collections.Generic.List[IO.FileSystemInfo]
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Program Files (x86)\Data1\' -r)) )
$items.AddRange( ([IO.FileSystemInfo[]](ls '\\server\C$\Web\DataStorage\' -r)) )
Jaykul
  • 14,264
  • 7
  • 55
  • 67
0

-Filter is more performant than -Include, so if you don't have a lot of different extensions, simply concatenating two filtered lists might be faster.

$files  = Get-ChildItem -Path "H:\stash\" -Filter *.rdlc -Recurse 
$files += Get-ChildItem -Path "H:\stash\" -Filter *.rdl  -Recurse 

I compared the output with a timer like this:

$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# Do Stuff Here
$stopwatch.Stop()
Write-Host "$([Math]::Round($stopwatch.Elapsed.TotalSeconds)) seconds ellapsed"
KyleMit
  • 45,382
  • 53
  • 367
  • 544