foreach-object — Run a script block on each pipeline object — like xargs across all 5 shells
Equivalents in every shell
command | xargs -I {} echo {}`xargs` with `-I {}` runs the template per input record — closest 1:1 mapping to `ForEach-Object { ... $_ ... }`. For multi-line bodies the more readable form is `command | while read -r line; do ...; done`. `for x in $(command); do ...; done` exists but breaks on whitespace in input — `while read` is the safer default.
command | while read -r line; do echo $line; doneSame `xargs` / `while read` idioms. Zsh adds the `${(f)...}` parameter flag to split a multi-line string into an array (`for f in ${(f)$(command)}; do ...; done`) — handles embedded whitespace better than the bash `for` loop. For pipelines, `while read -r` is still the standard.
command | while read -l line; echo $line; endFish `while read -l line ... end` is the native loop-per-line form (no need for `done`). For one-liners, fish's `for x in (command)` works correctly on whitespace because fish's command substitution splits on newlines, not on `$IFS`. xargs is also available.
Get-Process | ForEach-Object { $_.Name }PowerShell-native cmdlet (aliases `%` and `foreach`). The script block runs once per pipeline object with `$_` as current. `-Begin`, `-Process`, `-End` script blocks let you set up + tear down state. PS 3.0+ adds a member-access shorthand: `Get-Process | ForEach-Object Name` (project a property) and `Get-Process | ForEach-Object Kill` (invoke a method) — no `$_`, no braces.
for /f "delims=" %i in ('command') do @echo %i`for /f` iterates over command output line-by-line. `delims=` (empty) keeps each line whole (default delimiter is space/tab — bites unquoted paths). In batch files use `%%i` (double-percent). For per-file iteration use `for %i in (*.log) do ...`. cmd's loop syntax is less flexible than PowerShell's but covers the common cases.
Worked examples
Print the name of each running process
ps -o comm= -e | xargs -I {} echo {}ps -o comm= -e | while read -l name; echo $name; endGet-Process | ForEach-Object NameDelete every .tmp file under the current tree
find . -name "*.tmp" -print0 | xargs -0 rm -fGet-ChildItem -Recurse -Filter *.tmp | ForEach-Object { Remove-Item $_.FullName }for /r %i in (*.tmp) do @del "%i"Rename .txt files to .md
for f in *.txt; do mv -- "$f" "${f%.txt}.md"; donefor f in *.txt; mv $f (string replace -r .txt\$ .md $f); endGet-ChildItem *.txt | ForEach-Object { Rename-Item $_ ($_.BaseName + '.md') }Gotchas
- The PS 3.0+ shorthand `... | ForEach-Object Name` looks like a `Select-Object Name` but is NOT the same: `ForEach-Object Name` emits the raw `Name` STRING value; `Select-Object Name` wraps it in a `PSCustomObject` with a single `Name` property. Pipe to `Format-Table` and they look similar; pipe to `Set-Content` and they diverge sharply.
- `ForEach-Object` (the cmdlet) and the `foreach` statement (`foreach ($x in $coll) { ... }`) are DIFFERENT. The statement materialises the collection first (no streaming) and treats an empty input as zero iterations; the cmdlet streams but treats `$null` as one iteration with `$_ = $null`. Surprising on empty pipelines like `Get-ChildItem nonexistent\* | ForEach-Object { ... }`.
- `-Begin` and `-End` blocks run ONCE before / after the stream — useful for headers / footers / aggregation. They run even if the pipeline is empty. Common bug: putting initialisation in `-Process` instead of `-Begin` resets state per object.
- Inside the script block, `return` does NOT exit the loop — it just stops the current iteration (like `continue` in a normal loop). To stop a `ForEach-Object` early, throw or use `Select-Object -First N` upstream.
- Parallel execution: PS 7+ adds `ForEach-Object -Parallel { ... } -ThrottleLimit N` — but each parallel thread runs in a FRESH runspace. Variables from the caller scope are NOT visible unless you use `$using:varname`. This is a frequent first-time-pwsh-7 stumble.