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 do