Protect foreach loop when empty list

Using Powershell v2.0 I want to delete any files older than X days:

$backups = Get-ChildItem -Path $Backuppath | 
                Where-Object {($_.lastwritetime -lt (Get-Date).addDays(-$DaysKeep)) -and (-not $_.PSIsContainer) -and ($_.Name -like "backup*")}

foreach ($file in $backups)
{
    Remove-Item $file.FullName;
}

However, when $backups is empty I get: Remove-Item : Cannot bind argument to parameter 'Path' because it is null.

I've tried:

  1. Protecting the foreach with if (!$backups)
  2. Protecting the Remove-Item with if (Test-Path $file -PathType Leaf)
  3. Protecting the Remove-Item with if ([IO.File]::Exists($file.FullName) -ne $true)

None of these seem to work, what if the recommended way of preventing a foreach loop from being entered if the list is empty?


Solution 1:

With Powershell 3 the foreach statement does not iterate over $null and the issue described by OP no longer occurs.

From the Windows PowerShell Blog post New V3 Language Features:

ForEach statement does not iterate over $null

In PowerShell V2.0, people were often surprised by:

PS> foreach ($i in $null) { 'got here' }

got here

This situation often comes up when a cmdlet doesn’t return any objects. In PowerShell V3.0, you don’t need to add an if statement to avoid iterating over $null. We take care of that for you.

For PowerShell $PSVersionTable.PSVersion.Major -le 2 see the following for original answer.


You have two options, I mostly use the second.

Check $backups for not $null. A simple If around the loop can check for not $null

if ( $backups -ne $null ) {

    foreach ($file in $backups) {
        Remove-Item $file.FullName;
    }

}

Or

Initialize $backups as a null array. This avoids the ambiguity of the "iterate empty array" issue you asked about in your last question.

$backups = @()
# $backups is now a null value array

foreach ( $file in $backups ) {
    # this is not reached.
    Remove-Item $file.FullName
}

Sorry, I neglected to provide an example integrating your code. Note the Get-ChildItem cmdlet wrapped in the array. This would also work with functions which could return a $null.

$backups = @(
    Get-ChildItem -Path $Backuppath |
        Where-Object { ($_.lastwritetime -lt (Get-Date).addDays(-$DaysKeep)) -and (-not $_.PSIsContainer) -and ($_.Name -like "backup*") }
)

foreach ($file in $backups) {
    Remove-Item $file.FullName
}

Solution 2:

I know this is an old post but I'd like to point out that the ForEach-Objec cmdlet doesn't suffer the same issue as using the ForEach key word. So you can pipe the results of DIR to ForEach and just reference the file using $_, like:

$backups | ForEach{ Remove-Item $_ }

You can actually forward the Dir command itself through the pipe and avoid even assigning the variable like:

Get-ChildItem -Path $Backuppath | 
Where-Object {
             ($_.lastwritetime -lt (Get-Date).addDays(-$DaysKeep)) -and `
             (-not $_.PSIsContainer) -and ($_.Name -like "backup*")
             } |
ForEach{ Remove-Item $_ }

I added line breaks for readability.

I understand some people like ForEach/In for readability. Sometimes ForEach-Object can get a little hairy, especially if you are nesting as it gets hard to follow the $_ reference. At any rate, for a small operation like this it's perfect. Many people also assert it's faster however I've found that to be only slightly.

Solution 3:

I've developed a solution by running the query twice, once to get the files and once to count the files by casting the get-ChilItem to return an array (casting $backups as an array after the fact doesn't seem to work).
At least it works as expected (performance shouldn't be as issue since there'll never be more than a dozen files), if anyone knows of a single-query solution, please post it.

$count = @(Get-ChildItem -Path $zipFilepath | 
                Where-Object {($_.lastwritetime -lt (Get-Date).addDays(-$DaysKeep)) -and (-not $_.PSIsContainer) -and ($_.Name -like $partial + "*")}).count;

if ($count -gt 0)
{
    $backups = Get-ChildItem -Path $zipFilepath | 
                Where-Object {($_.lastwritetime -lt (Get-Date).addDays(-$DaysKeep)) -and (-not $_.PSIsContainer) -and ($_.Name -like $partial + "*")};

    foreach ($file in $backups)
    {
        Remove-Item $file.FullName;
    }
}