PowerShell Import-Csv and ForEach: Loop Through CSV Rows

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
PowerShell Import-Csv with a foreach loop over each row
One line of output per CSV row (PowerShell 7)

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 fileAll rows into memory firstOne row at a time
Current rowYour loop variable$_
Skip a rowcontinuereturn
Best forSmall and medium files you’ll reuseLarge 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
PowerShell Import-Csv values sort as text until you cast them to numbers
Text sort vs number sort (Windows PowerShell 5.1)

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
PowerShell continue inside ForEach-Object stops the whole script
Only one name, and the last line never runs (PowerShell 7)

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: