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:
- Protecting the foreach with
if (!$backups)
- Protecting the Remove-Item with
if (Test-Path $file -PathType Leaf)
- 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;
}
}