# For Every File: for Loops

A for loop runs the same commands once for every word, number or file in a list, so renaming 12 files or 200 takes one line, with ${f%.tmp} cutting the old ending off each name.

Apprentice · Unix · Linux · Updated November 10, 2026

## After this lesson you can

- Repeat commands for every word, number or file with a for loop
- Rename many files at once with a for loop, cutting the old ending off with ${f%.tmp}

## Key takeaways

- for name in list; do commands; done runs the commands once for each item in the list, with $name holding that item.
- The list can be words you type, numbers from braces such as {10..1}, or files from a wildcard such as telemetry/*.tmp.
- ${f%.tmp} is the value of f with .tmp cut off the end, so "${f%.tmp}.log" is the new name.
- Put echo in front of mv to see what a loop would do before it does it.

Your phone saved 200 vacation photos as `IMG_0001.JPG` to `IMG_0200.JPG`, and the website you're building wants them ending in a lowercase `.jpg`. In a file manager that's 200 renames: click, wait, type, Enter, 200 times. A data analyst with 50 reports to convert and a web developer with a folder of images to shrink face the same thing: one job, done once for each file. Bash has a word for "for each", and it's what turns a script from a list of commands into a small application that handles any number of files.

The examples are set at Mission Control on launch day. The telemetry files, the satellite's report from each orbit, came in with the wrong ending. Make the practice folder:

```bash
mkdir -p ~/terminal-practice/mission-control/telemetry
cd ~/terminal-practice/mission-control
touch telemetry/orbit-{01..12}.tmp
ls telemetry
```

```text
orbit-01.tmp  orbit-04.tmp  orbit-07.tmp  orbit-10.tmp
orbit-02.tmp  orbit-05.tmp  orbit-08.tmp  orbit-11.tmp
orbit-03.tmp  orbit-06.tmp  orbit-09.tmp  orbit-12.tmp
```

## Why one mv can't do it

Every file must end in `.log` instead of `.tmp`. A wildcard looks like the answer, but this fails:

```bash
mv telemetry/*.tmp telemetry/*.log
```

```text
mv: target 'telemetry/*.log': No such file or directory
```

The shell expands each pattern before `mv` runs (*Wildcards: \*, ? and [ ]*). `telemetry/*.tmp` became twelve names. `telemetry/*.log` matched nothing, so it stayed as it was, and `mv` took that last name as the folder to move the other twelve into. `mv` renames one file at a time, so it has to run twelve times, once for each file. That's a loop's job.

## for: the same commands for each item

A **loop** repeats commands. A `for` loop works like someone calling the roll: for each name on the list, do the same thing. Here is the launch team's go/no-go poll:

```bash
for system in fuel power radio; do
  echo "Checking $system... go"
done
```

```text
Checking fuel... go
Checking power... go
Checking radio... go
```

Read it aloud: "for each `system` in fuel, power and radio, do this, done." Each time round, the variable `system` holds the next word, and the commands between `do` and `done` run once with it. In a script you write a loop on several lines like this; typed at the prompt, the shell shows `>` on each new line until you type `done`. On one line, a `;` goes before `do` and before `done`.

## Counting with {10..1}

The list can be numbers. Brace expansion (*Brace Expansion: Twelve Folders in One Line*) writes a range for you: `{1..10}` is 1 to 10, and `{10..1}` counts down.

```bash
for n in {10..1}; do printf '%s ' "$n"; done; echo "Liftoff"
```

```text
10 9 8 7 6 5 4 3 2 1 Liftoff
```

`printf '%s '` prints each number followed by a space instead of a new line. One limit: the shell expands braces before it looks at variables, so a range can't come from a variable.

```bash
n=5
echo {1..$n}
```

```text
{1..5}
```

*Line by Line: while read* shows a loop that counts to any number.

## Every file: rename them all

The most useful list is files, from a wildcard. For each file, the new name is the old one with `.tmp` cut off and `.log` added. `${f%.tmp}` does the cutting: read it "f, with .tmp cut off the end". It's called **parameter expansion**: the shell changes a variable's value as it uses it.

```bash
f=telemetry/orbit-01.tmp
echo "${f%.tmp}.log"
```

```text
telemetry/orbit-01.log
```

Look before you rename. With `echo` in front, the loop prints each `mv` command instead of running it. `head -n 3` shows the first three of the twelve:

```bash
for f in telemetry/*.tmp; do echo mv "$f" "${f%.tmp}.log"; done | head -n 3
```

```text
mv telemetry/orbit-01.tmp telemetry/orbit-01.log
mv telemetry/orbit-02.tmp telemetry/orbit-02.log
mv telemetry/orbit-03.tmp telemetry/orbit-03.log
```

Those are right, so take the `echo` out:

```bash
for f in telemetry/*.tmp; do mv "$f" "${f%.tmp}.log"; done
ls telemetry
```

```text
orbit-01.log  orbit-04.log  orbit-07.log  orbit-10.log
orbit-02.log  orbit-05.log  orbit-08.log  orbit-11.log
orbit-03.log  orbit-06.log  orbit-09.log  orbit-12.log
```

Twelve files or 200, it's the same line. The double quotes around `"$f"` keep a name with a space in it in one piece (*Quotes: Double, Single and None*), and as with any wildcard, `*` skips hidden files, whose names start with a dot. Run the loop a second time, though, and `mv` complains:

```bash
for f in telemetry/*.tmp; do mv "$f" "${f%.tmp}.log"; done
```

```text
mv: cannot stat 'telemetry/*.tmp': No such file or directory
```

No file matched, so bash handed the pattern itself to the loop, which ran once with it. Nothing was harmed, and *Stopping Early: break, continue and Counters* shows how to skip it. In zsh on a Mac, a pattern that matches nothing stops the line with `no matches found` instead, and `mv` words its messages differently there.

## Try it in the terminal

[Terminal Quest: Mission Control, goal 3](https://www.ask-codey.com/games/terminal-quest/mission-control/bash/step): played right on the lesson page.

Your turn: Mission Control's thirty telemetry files end in `.tmp`. Rename every one of them to `.log` with one loop. One file hides from the wildcard: can you find it?

## Check yourself

2 questions to try on the lesson page.

## So what

A `for` loop does the same job to every item in a list, so a script that handles one file handles 200 with no extra work: the start of every tool that tidies, converts or backs up a folder.

## Key terms

- [for Loop](https://www.ask-codey.com/glossary/for-loop-unix)
- [Loop](https://www.ask-codey.com/glossary/loop-software-development)
- [Brace Expansion](https://www.ask-codey.com/glossary/brace-expansion)
- [Glob](https://www.ask-codey.com/glossary/glob)
- [Parameter Expansion](https://www.ask-codey.com/glossary/parameter-expansion)

## Cheat sheets

- [For Every File: for Loops](https://www.ask-codey.com/cheat-sheets/bash-for-loops-cheat-sheet)

## Related

- [Wildcards: \*, ? and \[ \]](https://www.ask-codey.com/lessons/bash-wildcards)
- [Brace Expansion: Twelve Folders in One Line](https://www.ask-codey.com/lessons/bash-brace-expansion)
- [Line by Line: while read](https://www.ask-codey.com/lessons/bash-while-read)
- [Stopping Early: break, continue and Counters](https://www.ask-codey.com/lessons/bash-loop-control)

Source: https://www.ask-codey.com/lessons/bash-for-loops
