For Each Thing: `foreach`

The foreach statement runs the steps in its braces once for each item in a list, with a variable you name holding this item, so a script renames 30 files or 200 with one loop and can decide about each one with an if.

Apprentice · PowerShell · Updated

After this lesson you can

  • Repeat steps for each item in a list with the foreach statement, naming a variable for this item
  • Rename or check every file in a folder with foreach, Rename-Item and an if inside the loop
  • Choose between the foreach statement and ForEach-Object for a job

Key takeaways

  • foreach ($file in a list) { … } runs the steps in the braces once for each item, and $file holds this item on each pass.
  • The list can be values you type, numbers such as 1..10, or the files Get-ChildItem finds.
  • Rehearse with -WhatIf on a few items first, then run the loop on all of them.
  • An if inside the loop decides about each item, such as keeping only the sensors hotter than 90.
  • Use foreach in a script, with several steps for each item. Use ForEach-Object in a pipeline, where each item flows on to the next command.

A program that saves reports crashed in the night, and 200 finished reports sit in a folder still ending in .tmp, the ending many programs give a file while they're writing it. Each one has to end in .log before anyone can open it. By hand, that's 200 renames. Doing Something to Each: ForEach-Object did a job like this in one line at the prompt. In a script, where each file may need several steps and a decision, PowerShell has a statement that reads like a sentence: foreach. An IT technician uses it to fix every computer on a list, and a data analyst to clean every file in a month of exports.

The examples are set at Mission Control on launch day, where 30 telemetry files, the satellite's report from each orbit, came in with the wrong ending. These lines make the practice folder with 30 empty files. Running them again is safe: -Force makes any file that's already there again, empty.

New-Item -ItemType Directory -Force -Path "$HOME\terminal-practice\mission-control\telemetry" | Out-Null
Set-Location "$HOME\terminal-practice\mission-control"
1..30 | ForEach-Object { New-Item "telemetry\orbit-$_.tmp" -Force } | Out-Null
Get-ChildItem telemetry -Name | Select-Object -First 3
orbit-1.tmp
orbit-10.tmp
orbit-11.tmp

The names sort as text, so orbit-10.tmp comes before orbit-2.tmp.

foreach: the same steps for each one

A loop repeats steps. Picture a flight controller going down the launch checklist: for each system on the list, ask the same question. The foreach statement does exactly that:

foreach ($system in 'fuel', 'power', 'radio') {
    "Checking $system... go"
}
Checking fuel... go
Checking power... go
Checking radio... go

Read it aloud: "for each system in fuel, power and radio, do what's in the braces." The parentheses hold a variable you name, $system, the word in, and the list. Each time round the loop, called a pass, $system holds the next item and the steps in the braces run once with it. Name the variable for one item: $file, $song, $sensor.

Typed at the prompt, PowerShell starts each new line with >> until the closing brace, and Enter after it runs the loop. In a script you write it on several lines, like this (Your First Script).

Rename every telemetry file

Now the list is files. Get-ChildItem telemetry\*.tmp finds them, and each new name is the file's BaseName, its name without the extension, plus .log. Rehearse on three first with -WhatIf, as in Doing Something to Each: ForEach-Object:

foreach ($file in Get-ChildItem telemetry\*.tmp | Select-Object -First 3) {
    Rename-Item $file.FullName ($file.BaseName + '.log') -WhatIf
}
What if: Performing the operation "Rename File" on target "Item: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-1.tmp Destination: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-1.log".
What if: Performing the operation "Rename File" on target "Item: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-10.tmp Destination: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-10.log".
What if: Performing the operation "Rename File" on target "Item: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-11.tmp Destination: C:\Users\Sam\terminal-practice\mission-control\telemetry\orbit-11.log".

$file.FullName is the file's whole path, so Rename-Item finds it wherever you run the loop from. The parentheses around ($file.BaseName + '.log') make PowerShell work out the new name before handing it over. The Destinations look right, so run it on all of them:

foreach ($file in Get-ChildItem telemetry\*.tmp) {
    Rename-Item $file.FullName ($file.BaseName + '.log')
}
Get-ChildItem telemetry -Name | Select-Object -First 3
(Get-ChildItem telemetry\*.log).Count
orbit-1.log
orbit-10.log
orbit-11.log
30

Thirty files or 200, it's the same three lines. Run the loop a second time and nothing happens, with no error: no file matches *.tmp any more, so the loop has no passes to make.

A decision on every pass

An if inside the loop (If This, Then That: if, elseif and else) decides about each item. Mission Control needs to know which sensors are running hot. Each sensor writes one line, such as temp=97. Make four:

New-Item -ItemType Directory -Force -Path sensors | Out-Null
Set-Content sensors\sensor-01.txt 'temp=72'
Set-Content sensors\sensor-02.txt 'temp=97'
Set-Content sensors\sensor-03.txt 'temp=90'
Set-Content sensors\sensor-04.txt 'temp=101'

For each sensor, read its line, take off temp= with the .Replace() method (Properties and Methods), turn what's left into a number with [int] (Numbers, Dates and Types), and output the sensor's name if it's over 90:

foreach ($sensor in Get-ChildItem sensors\*.txt) {
    $temp = [int](Get-Content $sensor.FullName).Replace('temp=', '')
    if ($temp -gt 90) {
        $sensor.BaseName
    }
}
sensor-02
sensor-04

sensor-03 is exactly 90, which isn't more than 90. Leave out [int] and sensor-04 goes missing: compared as text, '101' comes before '90', the way words sort in a dictionary. Save these lines as check.ps1 and you have a small application: a sensor check you can run every morning.

foreach or ForEach-Object?

Both repeat steps for each item, and you'll use both. Choose by where the work happens:

The foreach statement ForEach-Object
Fits best a script, several steps for each item a pipeline at the prompt
This item a name you choose, such as $file $_
How it works gets the whole list, then goes round works on each item as it arrives
Its output can't be piped straight on flows on to the next command

That last row catches people. A pipe can't follow a foreach loop's closing brace:

foreach ($file in Get-ChildItem sensors) { $file.Name } | Sort-Object
ParserError:
Line |
   1 |  foreach ($file in Get-ChildItem sensors) { $file.Name } | Sort-Object
     |                                                          ~
     | An empty pipe element is not allowed.

Save the loop's output in a variable first, $names = foreach (…) { … }, and pipe $names, or use ForEach-Object. One more trap: foreach is also a short name for ForEach-Object. After a pipe, | foreach { … } is ForEach-Object; at the start of a line, foreach (…) is the statement.

Windows PowerShell 5.1 runs every loop in this lesson the same way. On a Mac or Linux, paths use /, and *.tmp doesn't match ORBIT-1.TMP: capital letters count in file names there.

Try it in the terminal

Terminal Quest: Mission Control, goals 3 to 4: played right on the lesson page.

Your turn at Mission Control: rename all thirty telemetry files from .tmp to .log with one loop, then write check.ps1, a loop with an if that outputs every sensor hotter than 90, and send its output to the uplink. The game's hint reads the number with -replace 'temp=', '', an operator that makes the same swap as .Replace(). One telemetry file hides from the wildcard: can you find it?

Check yourself

2 questions to try on the lesson page.

So what

foreach turns "do this to every one of them" into a few readable lines in a script, and with an if inside, a script can decide about each file, sensor or song on its own.

Key terms

Cheat sheets

Related