This article will look at examples of using constructions to loop through all files and folders on a disk or in a specific directory (using the ForEach* and Get-ChildItem statements). You can widely use them in your PowerShell scripts.
Usually, iterating over all file system objects in a directory arises when you need to perform a specific action with all nested objects. For example, you need to delete, copy, move files, and add or replace lines in all files in the specific directory according to some criteria.
To learn more about the Get-ChildItem Cmdlet, visit our article on how to use the PowerShell Get-ChildItem cmdlet.
As the title suggests, we’ll show you example scripts to loop through files and folders and process each item.
Table of Contents
The Different ForEach* Loop Constructs in PowerShell
Before diving into the examples, let’s briefly review the different ForEach* loop constructs in PowerShell. These loops may be used in the succeeding examples.
ForEach Statement
The foreach statement could be the most popular loop in this list. It has a straightforward syntax and is the easiest to understand, even for beginners.
Below is the foreach statement structure.
foreach ($currentItem in $objectCollection) { # Code to execute for the $currentItem }
For example, the below code iterates through the $fruits array. Each item in the array is assigned to the $fruit variable.
$fruits = @("Apple", "Banana", "Cherry", "Avocado") foreach ($fruit in $fruits) { "I love $fruit" }
This loop statement allows you to use the break and continue statements to control the flow of the loop.
ForEach-Object Cmdlet
The first distinction of the ForEach-Object loop is that it is a cmdlet. Not a statement or method. It is essential to understand that distinction because the ForEach-Object cmdlet is pipeline-friendly.
The basic ForEach-Object usage is as follows:
$objectCollection | ForEach-Object { # Code to execute for each object in the pipeline }
For example, this code gets all Windows services (collection) and pipes it through the ForEach-Object cmdlet to display each service (current line object) name, start type, and status.
Get-Service | ForEach-Object { [pscustomobject]([ordered]@{ "Service Name" = $($_.Name) "Start Type" = $($_.StartType) "Status" = $($_.Status) }) }
And since it is pipeline-friendly, it is also ideal for processing a filtered collection. The example below functions like the previous one but with an added filtering step using the Where-Object cmdlet.
Get-Service | Where-Object { $_.StartType -eq 'Automatic' } | ForEach-Object { [pscustomobject]([ordered]@{ "Service Name" = $($_.Name) "Start Type" = $($_.StartType) "Status" = $($_.Status) }) }
This time, the ForEach-Object cmdlet received only the services whose StartType value is Automatic.
ForEach() Method
The last one on our list is the ForEach() method. Array objects in PowerShell have a built-in ForEach() method comparable with the ForEach-Object cmdlet’s usage.
For example, the code below creates an array ($numbers) and processes each item in the array using the object’s ForEach() method.
$numbers = @(1, 2, 3, 4, 5) $numbers.ForEach({ if ($_ % 2 -eq 0) { Write-Host "$_ is an even number" } else { Write-Host "$_ is an odd number" } })
Now that we’ve refreshed our ForEach* loops knowledge, let’s apply them to practical use-case examples.
Scripts Reminders
Before you go any further, please be reminded of the following:
- The scripts provided in this article can be downloaded from this GitHub repository.
- These scripts were created and tested for the specific examples in this tutorial.
- These scripts are to be considered boilerplate, and you can modify and improve them for your specific requirements.
- These scripts have basic error-handling logic but are NOT guaranteed to be error-free.
- These scripts are capable of the -WhatIf parameter. Use it for safely testing their functions.
- Some of these scripts include the function to move, rename, and delete files. These actions are permanent, and there’s no undo function.
- Be cautious when running these scripts; do not test them in live files and environments.
Deleting Files by Ages using PowerShell Script
Housekeeping is a staple in a system administrator’s routine. One example is maintaining an application’s log files and removing files reaching a certain age.
The below script called DeleteFilesByAge.ps1 deletes files older than the specified age in days. It also allows you to specify which file types to delete. The loop construct used in this script is the ForEach-Object cmdlet.
This script also supports the -WhatIf switch to safely test it without accidentally deleting files.
# DeleteFilesByAge.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # The top folder path [Parameter(Mandatory)] [String] $Folder, # The file age in days as threshold [Parameter(Mandatory)] [int] $FileAgeInDays, # Optional file extensions to include # eg. *.txt,*.pdf [Parameter()] [string[]] $FileExtension, # Switch to do a recursive deletion [Parameter()] [switch] $Recurse ) # Calculate the date ceiling. $ceilingDate = (Get-Date).AddDays(-$FileAgeInDays) # Compose the Get-ChildItem parameters. $fileSearchParams = @{ Path = $Folder Recurse = $Recurse File = $true Force = $true } if ($FileExtension) { $fileSearchParams += @{Include = $FileExtension } } # Get files and delete them. Get-ChildItem @fileSearchParams | Where-Object { $_.CreationTime -lt $ceilingDate } | # Process each file ForEach-Object { Try { Remove-Item $_.FullName -Force -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('WhatIf'))) { "Deleted: $($_.FullName)" | Out-Default } } Catch { "Failed: $($_.Exception.Message)" | Out-Default } }
Suppose I have the following directory structure and files.
To delete files older than seven (7) days, run the script as follows. In this example, I used the -WhatIf switch so that it only shows which files it would delete.
.\DeleteFilesByAge.ps1 -Folder .\dummy -FileAgeInDays 7 -Recurse -WhatIf
The screenshot below shows that there are four files older than seven days.
You can also filter which file types to delete. Let’s say I only want to delete files with that match *.log, *.pdf. The command to do so is:
.\DeleteFilesByAge.ps1 -Folder .\dummy -FileAgeInDays 7 -Recurse -FileExtension *.log,*.pdf -WhatIf
When you’re ready to delete the files, re-run the script without the -WhatIf switch.
.\DeleteFilesByAge.ps1 -Folder .\dummy -FileAgeInDays 7 -Recurse
Note. Learn how to query Microsoft SQL server with Invoke SqlCmd on PowerShell.
Cleaning Up Filenames using PowerShell Script
Suppose you have the following files, and your task is to find all files with the extension *.log in and remove the numbers from all filenames.
This script below, named CleanupFilename.ps1, retrieves files from the specified folder and its subfolders and renames each file by removing the numbers in their filename. This script uses the foreach statement to loop through the files to rename.
# CleanupFilename.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # The top folder path [Parameter(Mandatory)] [String] $Folder, # Optional file extensions to include # eg. *.txt,*.pdf [Parameter()] [string[]] $FileExtension, # Switch to do a recursive deletion [Parameter()] [switch] $Recurse ) # Compose the Get-ChildItem parameters. $fileSearchParams = @{ Path = $Folder Recurse = $Recurse File = $true Force = $true } if ($FileExtension) { $fileSearchParams += @{Include = $FileExtension } } # Get files and clean up the filenames. $fileCollection = Get-ChildItem @fileSearchParams foreach ($file in $fileCollection) { Try { $newName = $file.Name -replace "[0-9]", "" Rename-Item -Path $file.FullName -NewName $newName -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('WhatIf'))) { "Renamed: $($file.FullName) => $newName" | Out-Default } } Catch { "Failed: $($_.Exception.Message)" | Out-Default } }
In this example, I’ll run the script against the c:\dummy folder and its subfolders as indicated by the -Recurse switch.
.\CleanupFilename.ps1 -Folder c:\dummy -FileExtension *.log -Recurse
As you can see, the script looped through each file and removed the numbers in each filename.
However, it failed to rename one file because it would become a duplicate. The new filenames are shown below, except for the one that failed.
Perhaps you can improve the script to handle the duplicate filename possibilities?
Synchronizing Directories
Another practical use case is synchronizing the contents of two folders. In this example, we’ll use a script called SyncFolder.ps1. There are two loops in this script, both using the ForEach-Object cmdlet.
The first loop replicates the source folder’s folder structure to the destination folder. The second one performs the file copy operation.
# SyncFolder.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # The source folder [Parameter(Mandatory)] [String] $SourceFolder, # The destination [Parameter(Mandatory)] [String] $DestinationFolder, # Date since the last update [Parameter(Mandatory)] [datetime] $SinceLastUpdate, # Switch to do a recursive deletion [Parameter()] [switch] $Recurse ) #Region Replicate Directory Structure # Get a list of directories in the source folder (excluding files) $directories = Get-ChildItem -Path $sourceFolder -Directory -Recurse # Create corresponding directories in the destination folder foreach ($directory in $directories) { try { $destinationPath = Join-Path -Path $destinationFolder -ChildPath $directory.FullName.Substring($sourceFolder.Length) if (!(Test-Path $destinationPath)) { $null = New-Item -Path $destinationPath -ItemType Directory -Force -ErrorAction Stop } } catch { "Failed: $($_.Exception.Message)" | Out-Default return $null } } #EndRegion # Compose the Get-ChildItem parameters. $fileSearchParams = @{ Path = $SourceFolder Recurse = $Recurse File = $true Force = $true } # Get files. $fileCollection = Get-ChildItem @fileSearchParams | Where-Object { $_.LastWriteTime -gt $SinceLastUpdate } # Copy the selected files to the destination folder foreach ($file in $fileCollection) { Try { $destinationPath = Join-Path -Path $destinationFolder -ChildPath $file.FullName.Substring($sourceFolder.Length) Copy-Item -Path $file.FullName -Destination $destinationPath -Force -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('WhatIf'))) { "Copied: $($file.FullName) => $($destinationPath)" | Out-Default } } Catch { "Failed: $($_.Exception.Message)" | Out-Default } }
To use this script, run the following command. This command replicates the folder structure of C:\source_dir\ to C:\duplicate_dir\. Next, it copies the files updated within the last seven days.
.\SyncFolder.ps1 -SourceFolder C:\source_dir\ -DestinationFolder C:\duplicate_dir\ -SinceLastUpdate (Get-Date).AddDays(-7) -Recurse
Removing Empty Directories
Removing empty directories can help start organizing a messy folder structure. And for that, we can use any of the ForEach* loops.
This example script (PurgeEmptyDirectory.ps1) uses the ForEach() method to iterate and evaluate each directory and delete it if it is empty.
# PurgeEmptyDirectory.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # Top folder to search [Parameter(Mandatory)] [String] $Folder ) (Get-ChildItem $Folder -Recurse -Directory).ForEach( { $currentDir = $_ if ($currentDir.GetFileSystemInfos().Count -lt 1) { Try { Remove-Item $_.FullName -Force -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('WhatIf'))) { "Deleted: $($_.FullName)" | Out-Default } } Catch { "Failed: $($_.Exception.Message)" | Out-Default } } } )
In this example, I’m deleting empty directories under the C:folder. As you can see below, the subFolder2_1 is empty.
To run the script:
.\PurgeEmptyDirectory.ps1 -Folder C:\dummy
After deleting the empty C:\dummy\subFolder2\subFolder2_1 directory, its parent directory then becomes empty.
In this situation, you must re-run the script several times until all empty directories have been deleted.
Finding and Moving Large Files
Now, let’s move large files from one directory to another. This script example is practical when you must maintain a directory size and move larger files to another location with more space.
This script, called MoveLargeFiles.ps1, retrieves the files from the source directory and filters out those whose size is equal to or larger than a specified size. Next, it moves those filtered large files to the destination folder.
# MoveLargeFile.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # The top folder path to search [Parameter(Mandatory)] [String] $SourceFolder, # The destination folder [Parameter(Mandatory)] [String] $DestinationFolder, # File size threshold [Parameter(Mandatory)] [int64] $Size, # Switch to do a recursive deletion [Parameter()] [switch] $Recurse ) # Compose the Get-ChildItem parameters. $fileSearchParams = @{ Path = $SourceFolder Recurse = $Recurse File = $true Force = $true } # Find the large files $largeFiles = Get-ChildItem @fileSearchParams | Where-Object { $_.Length -gt $Size } # Moved the filtered large files $largeFiles | ForEach-Object { Try { Move-Item $_.FullName -Destination $DestinationFolder -Force -ErrorAction Stop if (!($PSBoundParameters.ContainsKey('WhatIf'))) { "Copied: $($_.FullName) => $($DestinationFolder)" | Out-Default } } Catch { "Failed: $($_.Exception.Message)" | Out-Default } }
In this example, I have the following files under the C:\dummy folder.
For example, here’s the command to move files whose size is equal to or larger than 1GB.
.\MoveLargeFile.ps1 -SourceFolder C:\dummy -DestinationFolder C:\large_files -Size 1GB
The files larger than 1GB have been moved.
Have you ever tried editing or deleting a file in a shared folder, and it wouldn’t let you? Or perhaps you want to know if another user opens a file? Let’s use the foreach loop to find out.
This script, called IsTheSharedFileOpen.ps1, connects to a remote computer (SMB host) where the shared file is hosted using CIMSession and filters the matching filename. Next, it iterates through each matching open file and shows the full path, the user with the file opened, and the source computer.
# IsTheSharedFileOpen.ps1 [CmdletBinding(SupportsShouldProcess)] param ( # The filename to search [Parameter(Mandatory)] [String] $Filename, # The file server containing the shared file [Parameter(Mandatory)] [String] $Server ) $session = New-CimSession -ComputerName $Server $remoteFiles = @(Get-SmbOpenFile -CimSession $session | Where-Object { $_.ShareRelativePath -eq $Filename }) foreach ($file in $remoteFiles) { $file | Select-Object @{n = "File"; e = { $_.Path } }, @{n = "User"; e = { $_.ClientUserName } }, @{n = "Source"; e = { $_.ClientComputerName } } }
In this example, I’ll query the “Documents.csv” file and see who has it open and where.
.\IsTheSharedFileOpen.ps1 -Filename Documents.csv -Server DC1
Per the below result, the Documents.csv filename is opened by the user THEITBROS\alpha using the computer with the 10.0.0.4 IP address.
Conclusion
Every programming and scripting language employs some version of loop constructs, and PowerShell is no different. This article shows you the different ForEach* loops and how to use them for different practical scenarios.
We’ve discussed and used the foreach statement, ForEach-Object cmdlet, and ForEach() method to loop through files and folders. With the right knowledge, you can use these loop mechanisms for the same use cases. Which one is your favorite?
2 comments
It seems that the remove empty directories is not working, because an expression is only allowed as the first element of a pipeline.
Indeed, there was an error in the previous PowerShell script. The PS code for deleting empty folders might look like this:
Get-ChildItem -Path C:\TargetFolder -Recurse -Force -Directory | Where-Object {$_.GetFileSystemInfos().Count -eq 0} | Remove-Item -Force -Recurse
Article has been updated, thanks!