PowerShell ForEach $file in $Files


I have a PowerShell script that gets a file listing and moves the files that meet a specific criteria. Why does the foreach loop run even if the object is empty?

I would assume if $i did not exist it would not run. But if there are no results in $filePath why does the forEach loop run once? I have worked around my issue, but I am curious and I tried searching, but I could not find the answer.

To get this to work I just check to make sure $filePath exists before the forEach loop.

For example, if ($filePath){…

 $filePath = Get-ChildItem -Path $sourceDir | Where-Object {! $_.PSIsContainer -AND ($_.Name -Match "^XXX_XXX*" -OR $_.Name -Match "^YYY_XX*")}

 ForEach($i in $filePath){
     $sfName =  $i.Name
     $sfDir = $i.Directory
     $tFileName = testFile $destDir $sfName
     $sFile = $sourceDir + $sfName
     $tFile = $destDir + $tFileName

     moveFile $sFile $tFile

Best Answer

This is a classic issue that has been fixed in PowerShell V3 IIRC. PowerShell's foreach loop will allow you to iterate over a scalar value e.g.:

foreach ($i in 1) {$i}

That is handy because many times the collection you iterate can contain 0, 1 or N items e.g.:

$files = Get-ChildItem c:\temp\*.txt
foreach ($file in $files) {$file}

In this case, Get-ChildItem could return 0, 1 or N files. N files is great, we can iterate over that. However if it returned 1 file and then you had to stick that one file in an array so that you could use foreach - well that would kind of suck. So foreach allows you to iterate over a scalar object. The problem occurs when Get-ChildItem returns 0 objects. In that case, an empty array is not assigned to $files. Instead $null is assigned. PowerShell considers $null a scalar and hence the foreach loop will execute once where the iterated value is $null. That kind of sucks. And I and many other folks told the team this early on, so in V3 they changed foreach to not iterate a scalar if the value is $null.

Another way to work around this in V1/V2 is to ensure the empty case generates an empty array instead of $null. You can do that using an @() array expression e.g.

$files = @(Get-ChildItem c:\temp\*.txt)
foreach ($file in $files) {$file}

This will work for 0, 1, and N files. It's works for 0 files because the @() will result in an empty array being assigned to $files. foreach will not execute its body when looping over an empty array.