Skip to content

PowerShell Loop Through Files

How to efficiently iterate through files in PowerShell using Get-ChildItem and ForEach-Object to process, rename, or filter files in bulk with strongly typed FileInfo objects.

Iterating through files in a directory is a core task in systems administration and automated workflows. In PowerShell, files are not treated as plain strings containing directory paths; instead, querying the filesystem emits strongly typed System.IO.FileInfo objects. A common automation task involves processing every file in a folder, such as renaming, filtering, converting, or archiving them in bulk. The standard pattern combines Get-ChildItem to retrieve files with ForEach-Object (or a foreach statement) to act on each one individually in a streaming, memory-efficient manner.

The Get-ChildItem and Pipeline Iteration Pattern

The standard idiom for processing files in PowerShell combines Get-ChildItem (which discovers files) with ForEach-Object (which processes each file individually via the pipeline):

Get-ChildItem -Path <Path> -Filter <Filter> -File | ForEach-Object { <Logic> }
Consolepowershell
Get-ChildItem -Path "C:\Reports" -Filter "*.csv" | ForEach-Object {
    Write-Output "Found file: $($_.Name)"
}

where:

  • -Path "C:\Reports" specifies the target directory.
  • -Filter "*.csv" restricts results to files matching the wildcard pattern.
  • $_ represents the current FileInfo object as it flows through the pipeline.
Outputplaintext
Found file: january.csv
Found file: february.csv
Found file: march.csv

File Object Properties

When iterating through files, the current object in the pipeline ($_ or $PSItem) provides direct access to rich file metadata:

Property Type Description
$_.Name string The file name including its extension (e.g., app.log).
$_.BaseName string The file name without its extension (e.g., app).
$_.Extension string The file extension including the leading dot (e.g., .log).
$_.FullName string The absolute path to the file on disk.
$_.Length long The file size expressed in raw bytes.
$_.LastWriteTime DateTime Timestamp when the file was last modified.
$_.DirectoryName string The parent folder path holding the file.
enumerate-files.ps1powershell
param (
    [string]$TargetDirectory = "$env:TEMP\LogDrop"
)

Get-ChildItem -Path $TargetDirectory -Filter "*.log" -File | ForEach-Object {
    [PSCustomObject]@{
        FileName     = $_.Name
        BaseName     = $_.BaseName
        SizeBytes    = $_.Length
        SizeKB       = [math]::Round($_.Length / 1KB, 2)
        LastModified = $_.LastWriteTime.ToString("yyyy-MM-dd HH:mm")
    }
}

where:

  • -Path $TargetDirectory specifies the folder to search.
  • -Filter "*.log" uses native filesystem provider pattern matching to isolate .log files.
  • -File ensures that directories matching the pattern are excluded from the output.
  • $_ captures each individual FileInfo object passing through the pipeline.
  • [math]::Round($_.Length / 1KB, 2) calculates the file size in kilobytes using the built-in 1KB numeric multiplier.

Execute the script from the console:

Consolepowershell
pwsh ./enumerate-files.ps1 -TargetDirectory "C:\AppLogs"
Outputplaintext
FileName           BaseName   SizeBytes SizeKB LastModified
--------           --------   --------- ------ ------------
audit-2024.log     audit-2024   1048576 1024.0 2024-10-24 14:15
service-trace.log  service-tr     52428   51.2 2024-10-24 16:30
web-access.log     web-access    204800  200.0 2024-10-24 18:02

Filtering Further with Where-Object

Combine Where-Object in the pipeline to narrow down files by additional criteria beyond a simple wildcard.

Consolepowershell
Get-ChildItem -Path "C:\Reports" -File | Where-Object { $_.LastWriteTime -gt (Get-Date).AddDays(-7) } | ForEach-Object {
    Write-Output "$($_.Name) was modified in the last 7 days."
}
Outputplaintext
march.csv was modified in the last 7 days.

Recursive File Search (-Recurse)

To traverse an entire directory tree including all nested child folders, add the -Recurse switch. You can optionally restrict recursion depth using -Depth:

Consolepowershell
Get-ChildItem -Path "C:\Projects" -Filter "*.json" -File -Recurse -Depth 2

Practical Example: Batch Renaming Files

A common real-world task is renaming multiple files based on a specific pattern, for instance, adding a date prefix to every file in a folder.

batch-rename.ps1powershell
$targetFolder = "C:\Reports"
$datePrefix = Get-Date -Format "yyyy-MM-dd"

Get-ChildItem -Path $targetFolder -File | ForEach-Object {
    $newName = "$datePrefix`_$($_.Name)"
    Rename-Item -Path $_.FullName -NewName $newName
    Write-Output "Renamed $($_.Name) to $newName"
}

where:

  • $_.FullName provides the complete path required by Rename-Item.
  • The backtick (`) before the underscore escapes it, preventing PowerShell from interpreting $datePrefix_ as a single (nonexistent) variable name.
Consolepowershell
./batch-rename.ps1
Outputplaintext
Renamed january.csv to 2024-03-14_january.csv
Renamed february.csv to 2024-03-14_february.csv
Renamed march.csv to 2024-03-14_march.csv

Batch Rename with Structured Logging

A more advanced pattern uses Rename-Item with -PassThru to emit confirmation objects back into the pipeline for logging:

batch-rename-logs.ps1powershell
param (
    [string]$DirectoryPath = "$PSScriptRoot\Staging",
    [string]$Prefix        = "ARCHIVE"
)

if (-not (Test-Path -Path $DirectoryPath)) {
    Write-Error "Target directory '$DirectoryPath' does not exist."
    exit 1
}

$CurrentDate = Get-Date -Format "yyyyMMdd"

Get-ChildItem -Path $DirectoryPath -Filter "*.txt" -File | ForEach-Object {
    $NewFileName = "{0}_{1}_{2}.log" -f $Prefix, $CurrentDate, $_.BaseName

    Rename-Item -Path $_.FullName -NewName $NewFileName -PassThru | ForEach-Object {
        [PSCustomObject]@{
            OriginalName = $_.OldName # Retained via pipeline context
            NewName      = $_.Name
            Destination  = $_.FullName
            RenamedAt    = (Get-Date -Format "HH:mm:ss")
        }
    }
}

where:

  • Test-Path checks whether the target folder exists before attempting modifications.
  • Get-ChildItem collects .txt files in the staging folder.
  • "{0}_{1}_{2}.log" -f ... uses composite string formatting to build the standardized new filename.
  • Rename-Item -Path $_.FullName -NewName $NewFileName renames the file on disk.
  • -PassThru outputs the modified file object back into the pipeline for confirmation logging.

Execute the script from the console:

Consolepowershell
pwsh ./batch-rename-logs.ps1 -DirectoryPath "C:\Scripts\Staging" -Prefix "PROD"
Outputplaintext
NewName                         Destination                          RenamedAt
-------                         -----------                          ---------
PROD_20241024_trace-event.log   C:\Scripts\Staging\PROD_20241024...  22:45:10
PROD_20241024_user-action.log   C:\Scripts\Staging\PROD_20241024...  22:45:10

Practical Example: Batch Processing File Contents

Beyond renaming, this pattern is also highly useful for reading and transforming file contents in bulk. For example, you could use it to convert every .txt file in a folder to uppercase content.

batch-process.ps1powershell
$targetFolder = "C:\Data\Incoming"
$outputFolder = "C:\Data\Processed"

if (-not (Test-Path $outputFolder)) {
    New-Item -ItemType Directory -Path $outputFolder | Out-Null
}

Get-ChildItem -Path $targetFolder -Filter "*.txt" | ForEach-Object {
    $content = Get-Content -Path $_.FullName -Raw
    $processedContent = $content.ToUpper()
    $outputPath = Join-Path $outputFolder $_.Name
    Set-Content -Path $outputPath -Value $processedContent
    Write-Output "Processed: $($_.Name)"
}

where:

  • -Raw reads the entire file as a single string, rather than an array of lines.
  • Join-Path safely combines the output folder and filename into a full path, avoiding manual string concatenation errors.
Consolepowershell
./batch-process.ps1
Outputplaintext
Processed: notes.txt
Processed: readme.txt
Navigation