To loop through a CSV file in PowerShell, read it with Import-Csv and use a foreach loop. Each row becomes an object, and each column header becomes a property:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
foreach ($customer in $customers) {
"$($customer.Name) from $($customer.State) spent `$$($customer.Total)"
}
Output:
Alice Johnson from TX spent $1250
Bob Smith from CO spent $980
Carol Davis from WA spent $15000
Dan Miller from FL spent $425

The file customers.csv has the columns Id, Name, Company, State and Total, with four customers. I use it in every example below.
I ran each script in PowerShell 7.6 and Windows PowerShell 5.1. The results matched except for one case with single-row files, which I show below.
foreach vs ForEach-Object with Import-Csv
You can also pipe Import-Csv straight into ForEach-Object, where $_ is the current row:
Import-Csv -Path C:\psfaqs\customers.csv | ForEach-Object {
'{0}. {1} ({2})' -f $_.Id, $_.Name, $_.Company
}
Output:
1001. Alice Johnson (Acme, Inc.)
1002. Bob Smith (Contoso)
1003. Carol Davis (Fabrikam)
1004. Dan Miller (Northwind)
| foreach ($row in $data) | Import-Csv | ForEach-Object | |
|---|---|---|
| Reads the file | All rows into memory first | One row at a time |
| Current row | Your loop variable | $_ |
| Skip a row | continue | return |
| Best for | Small and medium files you’ll reuse | Large files you read once |
For most CSV files, pick whichever reads better to you. On a file with millions of rows, the pipeline keeps memory use low because it never holds the whole file. ForEach-Object has more on the pipeline version.
Import a CSV file into an array
Import-Csv already returns an array, so you can count rows, pick one by index or grab a whole column:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
"Type: $($customers.GetType().Name)"
"Rows: $($customers.Count)"
"First: $($customers[0].Name)"
"Last: $($customers[-1].Name)"
"Names: $($customers.Name -join ', ')"
Output:
Type: Object[]
Rows: 4
First: Alice Johnson
Last: Dan Miller
Names: Alice Johnson, Bob Smith, Carol Davis, Dan Miller
$customers.Name collects the Name of every row. It’s the quickest way to get a single column as a list.
Why .Count is empty for a one-row CSV
If the file has only one data row, you get a single object, not an array. In Windows PowerShell 5.1, .Count on that object returns nothing:
$one = Import-Csv -Path C:\psfaqs\one-customer.csv
"Without @(): $($one.GetType().Name), Count = [$($one.Count)]"
$one = @(Import-Csv -Path C:\psfaqs\one-customer.csv)
"With @(): $($one.GetType().Name), Count = [$($one.Count)]"
Output in Windows PowerShell 5.1:
Without @(): PSCustomObject, Count = []
With @(): Object[], Count = [1]
PowerShell 7 returns 1 in both cases, but wrapping the call in @() keeps your script safe in both versions. I do it whenever a file might have a single row.
Why don’t numbers from a CSV sort correctly?
Every value from Import-Csv is a string. Sorting by Total compares text, so 15000 lands before 425:
$customers = Import-Csv -Path C:\psfaqs\customers.csv
"Sorted as text: $(($customers | Sort-Object -Property Total).Total -join ', ')"
"Sorted as numbers: $(($customers | Sort-Object -Property { [int]$_.Total }).Total -join ', ')"
"Total spent: `$$(($customers | Measure-Object -Property Total -Sum).Sum)"
Output:
Sorted as text: 1250, 15000, 425, 980
Sorted as numbers: 425, 980, 1250, 15000
Total spent: $17655

Cast the value with [int], or [decimal] for dollars and cents, before you sort or compare it. Measure-Object converts numbers for you, so the sum is right either way.
Read a CSV line by line without breaking quoted values
It’s tempting to read the file with Get-Content and split each line on commas. That breaks as soon as a value contains a comma:
Get-Content -Path C:\psfaqs\customers.csv | Select-Object -Skip 1 -First 1 | ForEach-Object {
"Split(','): $($_.Split(',')[2])"
}
$first = Import-Csv -Path C:\psfaqs\customers.csv | Select-Object -First 1
"Import-Csv: $($first.Company)"
Output:
Split(','): "Acme
Import-Csv: Acme, Inc.
Import-Csv handles quoted fields correctly, and piping it to ForEach-Object already processes the file line by line. You rarely need Get-Content or a .NET StreamReader for CSV data.
CSV files with no header or a different delimiter
-Header names the columns when the file has no header row, and -Delimiter sets the separator:
Import-Csv -Path C:\psfaqs\no-header.csv -Delimiter ';' -Header 'Id', 'Full Name', 'City' |
ForEach-Object { "$($_.'Full Name') lives in $($_.City)" }
Output:
Alice Johnson lives in Austin
Bob Smith lives in Denver
For a column name with a space, like Full Name, put the property name in quotes: $_.'Full Name'. The Import-Csv documentation lists the other options, including -Encoding.
Don’t use continue inside ForEach-Object
This one surprises people. continue in a ForEach-Object script block doesn’t skip the row. It stops the whole script:
Import-Csv -Path C:\psfaqs\customers.csv | ForEach-Object {
if ($_.State -eq 'CO') { continue }
$_.Name
}
'This line never runs'
Output:
Alice Johnson

Only Alice printed, and the line after the loop never ran. Inside ForEach-Object, use return to skip a row:
Import-Csv -Path C:\psfaqs\customers.csv | ForEach-Object {
if ($_.State -eq 'CO') { return }
$_.Name
}
'Done'
Output:
Alice Johnson
Carol Davis
Dan Miller
Done
In a foreach statement, continue works the way you’d expect.
Add a column and save the results
Select-Object with a calculated property adds a new column, and Export-Csv writes it to a new file:
Import-Csv -Path C:\psfaqs\customers.csv |
Select-Object -Property Name, Total, @{ Name = 'Tier'; Expression = { if ([int]$_.Total -ge 1000) { 'Gold' } else { 'Standard' } } } |
Export-Csv -Path C:\psfaqs\customer-tiers.csv -NoTypeInformation
Get-Content -Path C:\psfaqs\customer-tiers.csv
Output:
"Name","Total","Tier"
"Alice Johnson","1250","Gold"
"Bob Smith","980","Standard"
"Carol Davis","15000","Gold"
"Dan Miller","425","Standard"
-NoTypeInformation matters in Windows PowerShell 5.1, where Export-Csv otherwise adds a #TYPE line at the top. PowerShell 7 leaves it out by default. See PowerShell Export-Csv for more.
Frequently Asked Questions
How do I loop through a CSV file in PowerShell?
Use foreach ($row in Import-Csv -Path file.csv) { $row.Name }, or pipe it: Import-Csv file.csv | ForEach-Object { $_.Name }.
How do I import a CSV file into an array in PowerShell?
$data = Import-Csv -Path file.csv gives you an array of objects. Wrap it in @() so a one-row file is still an array.
How do I skip a row in Import-Csv | ForEach-Object?
Use return. continue inside ForEach-Object stops the whole script, not just the current row.
Why are numbers from Import-Csv sorted wrong?
They’re strings. Cast them, like Sort-Object { [int]$_.Total }, to sort and compare them as numbers.
How do I import a CSV file without headers?
Pass the column names to -Header, like Import-Csv file.csv -Header 'Id', 'Name', 'City'. Add -Delimiter if it isn’t comma-separated.
Working with CSV files often leads to these next:
- Export data to CSV with PowerShell
- Convert JSON to CSV
- Convert Excel files to CSV
- PowerShell ForEach-Object
Bijay Kumar is an esteemed author and the mind behind PowerShellFAQs.com, where he shares his extensive knowledge and expertise in PowerShell, with a particular focus on SharePoint projects. Recognized for his contributions to the tech community, Bijay has been honored with the prestigious Microsoft MVP award. With over 15 years of experience in the software industry, he has a rich professional background, having worked with industry giants such as HP and TCS. His insights and guidance have made him a respected figure in the world of software development and administration. Read more.