In PowerShell scripts, it is often necessary to perform a particular action on all the files and subfolders in a directory. For example, delete, copy, move, or edit files according to a set of criteria. In this post we’ll show how to loop through files and folders and process each item using ForEach and Get-ChildItem PowerShell statements.
PowerShell Version Compatibility
The examples in our article are compatible with Windows PowerShell 5.1 and PowerShell 7.x unless stated otherwise.
- PowerShell 7.6 is the current Long-Term Servicing (LTS) release and is built on .NET 10. It is supported until November 2028.
- PowerShell 7.5 is the stable release based on .NET 9, with support ending in November 2026. PowerShell 7.4, the previous LTS release based on .NET 8, also reaches end of support in November 2026.
Note that you should test file-processing scripts in the target PowerShell version before deploying them in production.
How to loop through files and folders using ForEach and Get-ChildItem
To get a list of files in a particular directory (including child objects) and perform some action on each of them, you can use a loop structure like this:
Get-ChildItem -Path "C:\PS" -File -Recurse |ForEach-Object {#Do something with $_.FullName} This approach uses PowerShell pipeline streaming. In this case, objects are processed one-by-one and are not fully loaded into memory (this makes it efficient for large folders).
Note that by default Get-ChildItem -Recurse returns both files and folders. In case your script is intended to process files only, you should use the -File parameter to avoid unexpected behavior when folders are passed through the pipeline.
Keep in mind that recursive enumeration on large file systems may significantly impact performance and can traverse symbolic links, junctions, or protected folders. You should consider using filters, depth limits, and proper error handling in your production scripts.
In PowerShell 7+, be careful when using the -FollowSymlink switch because symbolic links and junction points may introduce recursive traversal loops/unexpected filesystem expansion.
For example, to recursively loop through a directory and remove all temporary files with *.LOG and *.TMP extensions from the directory and subfolders, use the following pipeline:
Get-ChildItem -Path "C:\PS" -File -Recurse |
Where-Object { $_.Extension -in '.tmp', '.log' } |
Remove-Item -WhatIf

The WhatIf parameter is used because we don’t really want to delete any files.
If you need to loop through each file, retrieve and process additional object attributes, you can use another foreach statement structure:
$allItems = Get-ChildItem -Path "C:\PS" -File -Recurse
foreach($item in $allItems)
{
# Do something with each file
}
In this approach, Get-ChildItem fully loads all objects into memory before the loop starts (materialization). This can increase memory usage significantly on large file systems.
For example, let’s create a PSCustomObject that contains the name, full path, and size of each file in the folder. This script also converts the file length value to Mb and sorts the files by size.
$AllFiles = foreach($item in (Get-ChildItem -Path "C:\PS" -File -Recurse))
{
[PSCustomObject]@{
Name = $item.Name
Size = [math]::Round($item.Length / 1MB, 2)
Path = $item.FullName
}
}
$AllFiles | Sort-Object Size -Descending
Note that you should avoid using += with arrays in PowerShell loops. Each append operation recreates the entire array in memory, which can severely impact performance on large collections.

Next, let’s look at some practical examples of PowerShell scripting when you can use foreach loops to perform some actions against all objects in a directory.
Golden rule for PowerShell loops
- Use pipeline when you need to process large datasets, filter objects, and for single-pass operations.
- Use foreach when multiple properties are reused, complex per-object logic is required, or per-iteration CPU efficiency matters more than memory consumption.
Parallel processing with ForEach-Object -Parallel
Note that PowerShell 7.0 and later supports ForEach-Object -Parallel, which can process independent items concurrently. For production environments, PowerShell 7.6 LTS is a supported long-term servicing option. However, you should test parallel execution behavior against the PowerShell version and workload used in your environment.
Here is an example:
Get-ChildItem -Path "C:\PS" -File -Recurse |
ForEach-Object -Parallel {
# Process $_
} -ThrottleLimit 8
The -ThrottleLimit parameter controls how many script blocks can run concurrently. But do not assume that increasing it always improves performance! File-system workloads can become slower when too many operations compete for the same disk, network share, or storage system.
You should use -Parallel only when operations are independent and parallel execution is safe. You need to avoid it when files must be processed in a specific order, when operations modify shared state, or when the workload is already limited by storage I/O.
Enterprise scripting recommendations
- You should use -ErrorAction for predictable behavior
- You need to avoid += array accumulation
- You must prefer pipeline streaming for large datasets
- Don’t forget to use -WhatIf before destructive operations
- You need to avoid unrestricted -Recurse on large file systems
Keep in mind that you should prefer using native Get-ChildItem filters (-Filter, -Include, -Exclude) whenever possible instead of filtering later with Where-Object. Provider-side filtering works significantly faster and reduces pipeline overhead on large file systems.
When processing paths from external input or recursive enumeration, we recommend you to use -LiteralPath instead of -Path to avoid wildcard interpretation problems. Also, you should avoid recursive scans from filesystem root paths such as C:\ in production environments unless it’s absolutely necessary.
Delete files in a directory that are older than xx days
Get-ChildItem C:\ps\ -File -Recurse |
Where-Object { $_.LastWriteTime -lt ($(Get-Date).AddDays(-10)) } |
ForEach-Object { Remove-Item -LiteralPath $_.FullName -WhatIf }
The one above is a streaming pipeline approach. In such case, filtering happens before objects are passed to Remove-Item (which prevents loading unnecessary items into memory).
Remove empty directories
Note that the following command performs an additional enumeration for each folder and may become slow on very large folder trees.
This is a one-pass approach that only removes directories that are empty at the time they are evaluated. If deleting a child directory causes its parent directory to become empty, the parent directory will not be removed until the script is run again. For complete cascading cleanup, you should process directories from the deepest level first or repeat the operation until no empty directories remain.
Get-ChildItem -Path 'C:\ps' -Directory -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object {
try {
@(Get-ChildItem -LiteralPath $_.FullName -Force -ErrorAction Stop).Count -eq 0
}
catch {
Write-Warning "Cannot inspect '$($_.FullName)': $($_.Exception.Message)"
$false
}
} |
ForEach-Object {
Remove-Item -LiteralPath $_.FullName -Force -WhatIf
}
Find large files (over 1GB) and move them to another folder
$filesize=1024*1024*1024
Get-ChildItem "D:\ISOs" -Filter *.iso -File -Recurse -ErrorAction SilentlyContinue |Where-Object { $_.Length -gt $filesize} |ForEach-Object { Move-Item -LiteralPath $_.FullName -Destination D:\ISO -WhatIf}
Replace a specific value in each TXT file
$allFiles = Get-ChildItem -LiteralPath 'C:\PS' -File -Recurse -Filter '*.TXT'
foreach ($file in $allFiles) {
$content = Get-Content -LiteralPath $file.FullName
$newContent = $content -replace 'old_value', 'new_value'
Set-Content -LiteralPath $file.FullName -Value $newContent -Encoding UTF8
}
Note that assigning the output of Get-Content to a variable loads the file contents into memory. This method may become inefficient for very large text/log files. Our example also rewrites the entire file, so specify the encoding explicitly when the file is consumed by apps that expect a particular encoding.
You should be careful when rewriting existing text files: encoding behavior differs between Windows PowerShell 5.1 and PowerShell 7. In case the original file encoding must be preserved, you need to determine the encoding first and specify it explicitly. Do not rely on the default encoding when processing files in a mixed PowerShell 5.1 and PowerShell 7 environment.
Rename each *.LOG file in a directory
Get-ChildItem -Path "C:\PS" -File -Filter *.LOG -Recurse |
Rename-Item -NewName { $_.Name -replace '\.LOG$', '.OLD' } -WhatIf
Copy Updated Files Between Directories
Another practical use case is copying updated files between two folders while preserving the source folder structure. The SyncFolder.ps1 script uses two foreach language loops (not pipeline ForEach-Object processing). The first loop replicates the source folder structure to the destination folder, while the second loop copies updated files.
# 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,
# Process files recursively in subdirectories
[Parameter()]
[switch]
$Recurse
)
#Region Replicate Directory Structure
# Get a list of directories in the source folder (excluding files)
$directorySearchParams = @{
Path = $SourceFolder
Directory = $true
Force = $true
ErrorAction = 'Stop'
}
if ($Recurse) {
$directorySearchParams.Recurse = $true
}
$directories = Get-ChildItem @directorySearchParams
# Create corresponding directories in the destination folder
foreach ($directory in $directories) {
try {
$destinationPath = Join-Path -Path $destinationFolder -ChildPath $directory.FullName.Substring($SourceFolder.Length)
if (-not (Test-Path -LiteralPath $destinationPath -PathType Container)) {
if ($PSCmdlet.ShouldProcess($destinationPath, "Create Directory")) {
$null = New-Item `
-LiteralPath $destinationPath `
-ItemType Directory `
-Force `
-ErrorAction Stop
}
}
}
catch {
Write-Warning "Failed to create '$destinationPath': $($_.Exception.Message)"
continue
}
}
#EndRegion
# Compose the Get-ChildItem parameters.
$fileSearchParams = @{
Path = $SourceFolder
Recurse = $Recurse
File = $true
Force = $true
ErrorAction = 'Stop'
}
# Get files
try {
$fileCollection = Get-ChildItem @fileSearchParams |
Where-Object { $_.LastWriteTime -gt $SinceLastUpdate }
}
catch {
throw "Failed to enumerate source files: $($_.Exception.Message)"
}
# Copy the selected files to the destination folder
foreach ($file in $fileCollection) {
Try {
$destinationPath = Join-Path -Path $destinationFolder -ChildPath $file.FullName.Substring($SourceFolder.Length)
if ($PSCmdlet.ShouldProcess($destinationPath, "Copy File")) {
Copy-Item `
-LiteralPath $file.FullName `
-Destination $destinationPath `
-Force `
-ErrorAction Stop
"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

Why is pipeline streaming important in PowerShell?
Pipeline streaming allows PowerShell to process objects one at a time instead of loading the entire collection into memory. This significantly reduces memory usage on large file systems.
Why should I avoid using += with arrays in loops?
Using += recreates the entire array in memory during every append operation. This becomes very inefficient on large collections.
What is the difference between ForEach-Object and foreach in PowerShell?
- ForEach-Object works in the PowerShell pipeline and processes objects one-by-one (streaming mode). It is more memory-efficient and better suited for large datasets.
- foreach is a PowerShell language construct that processes a fully materialized collection already loaded into memory.

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!
That script to synchronize folders looks like just what I need – to duplicate a folder structure and files in it from one drive to another. Is that what it already does or would a change need to be made to force a different destination drive? I am basically trying to back up all my multimedia files to a Windows Storage Pool.