/commands/bash/loop-through-files-in-directory
bash loop through files in a directory (for f in *.txt)
- tool
- Bash
- versions
- Bash 3.2+, globstar: Bash 4.0+
- os
- linux, macos, windows
for f in /path/to/dir/*.txt; do echo "$f"; done loops over every .txt file in a directory. The glob is expanded by bash itself, so names with spaces stay intact as long as you quote "$f" inside the loop. Set shopt -s nullglob so the loop does not run when nothing matches, and use **/*.txt with shopt -s globstar (Bash 4+) or find to include subdirectories.
Loop, filter, recurse
mkdir -p data/sub
touch "data/a.txt" "data/b c.txt" "data/notes.md" "data/sub/deep.txt" "data/.hidden.txt"
echo "--- *.txt in data"
for f in data/*.txt; do
echo "file: $f (name: ${f##*/}, stem: $(basename "$f" .txt))"
done
echo "--- no match, default"
for f in data/*.csv; do echo "got: $f"; done
echo "--- no match, nullglob"
shopt -s nullglob
for f in data/*.csv; do echo "got: $f"; done
shopt -u nullglob
echo "--- recursive with globstar"
shopt -s globstar
for f in data/**/*.txt; do echo "$f"; done
echo "--- only regular files, any name"
for f in data/*; do
[ -f "$f" ] || continue
echo "$f"
doneOutput
--- *.txt in data file: data/a.txt (name: a.txt, stem: a) file: data/b c.txt (name: b c.txt, stem: b c) --- no match, default got: data/*.csv --- no match, nullglob --- recursive with globstar data/a.txt data/b c.txt data/sub/deep.txt --- only regular files, any name data/a.txt data/b c.txt data/notes.md
The hidden file is never matched by *, the unmatched *.csv runs the loop once with the pattern itself unless nullglob is on, and [ -f "$f" ] skips the sub directory.
Variants
| Code | What it does |
|---|---|
| for f in dir/*; do ...; done | Every entry (files and directories) in dir, sorted by name |
| for f in dir/*.{jpg,png}; do | Several extensions with brace expansion |
| shopt -s nullglob | Patterns that match nothing expand to nothing |
| shopt -s dotglob | Include hidden files in * |
| shopt -s globstar; for f in dir/**/*.txt | Recurse into subdirectories (Bash 4.0+) |
| find dir -type f -name "*.txt" -print0 | while IFS= read -r -d "" f; do | Recursive, any file name, works on Bash 3.2 (macOS) |
| for f in "$dir"/*; do | Quote the variable part, leave the * outside the quotes |
Pitfalls
- Unquoted
$f.cp $f backup/splitsb c.txtinto two arguments; redirections such as> $f.outfail with ambiguous redirect. Always write"$f". - Quoted glob.
for f in "*.txt"loops once over the literal string; the*must be outside the quotes. for f in $(ls)or$(find ...)breaks on spaces and newlines in names. Use the glob, orfind -print0withread -d "".- macOS bash 3.2 has no
globstar;shopt -s globstarfails withinvalid shell option name. Usefind, or zsh, where**works by default. - Modifying the directory while looping (renaming or creating files) is safe with a glob, because the list is built once before the loop starts.
Confirm it worked
Run the loop with echo in front of the real command first (echo mv "$f" ...) to see exactly what it would do, then remove the echo.
FAQ
Why does my loop run once with the literal *.txt?
When a glob matches nothing, bash leaves the pattern unchanged, so the loop body runs once with f set to *.txt. Enable shopt -s nullglob, or test [ -e "$f" ] || continue at the top of the loop.
Should I use for f in $(ls)?
No. The output of ls is split on spaces and newlines and then globbed again, so file names with spaces or wildcards break. A plain glob, for f in *, hands each name over intact.
Does * include hidden files?
No. Names starting with a dot are skipped unless the pattern starts with a dot (.*) or shopt -s dotglob is set.