Skip to content

/commands/bash/loop-through-files-in-directory

bash loop through files in a directory (for f in *.txt)

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
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

bash
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"
done

Output

--- *.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

CodeWhat it does
for f in dir/*; do ...; doneEvery entry (files and directories) in dir, sorted by name
for f in dir/*.{jpg,png}; doSeveral extensions with brace expansion
shopt -s nullglobPatterns that match nothing expand to nothing
shopt -s dotglobInclude hidden files in *
shopt -s globstar; for f in dir/**/*.txtRecurse into subdirectories (Bash 4.0+)
find dir -type f -name "*.txt" -print0 | while IFS= read -r -d "" f; doRecursive, any file name, works on Bash 3.2 (macOS)
for f in "$dir"/*; doQuote the variable part, leave the * outside the quotes

Pitfalls

  • Unquoted $f. cp $f backup/ splits b c.txt into two arguments; redirections such as > $f.out fail 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, or find -print0 with read -d "".
  • macOS bash 3.2 has no globstar; shopt -s globstar fails with invalid shell option name. Use find, 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.

Sources