Bulk rename a batch of files

A plain bash loop, spaces handled, previewed first

Updated 2026-07-05

Problem: A folder of files needs a consistent rename, like lowercasing extensions or adding a shared prefix, and doing it by hand one file at a time isn't practical.

Solution:

for f in *.JPG; do
  mv -- "$f" "${f%.JPG}.jpg"
done
ls *.jpg
IMG_0001.jpg
IMG_0002.jpg
IMG_0003.jpg

How it works:

  • for f in *.JPG expands to every matching filename in the current directory, one per iteration.
  • ${f%.JPG} strips the literal suffix .JPG from the end of $f (a parameter expansion, not a regex), leaving the base name to rebuild with a new extension.
  • mv itself prints nothing on success, which is why the loop ends with ls *.jpg. The listing above is the proof the rename happened, not output from the loop.
  • mv -- "$f" "..." renames the file. The -- tells mv that no more flags follow, which matters if a filename happens to start with a -. Quoting "$f" is what makes this safe for filenames containing spaces (see Variables and quoting). The same job built as a pipeline needs xargs -0 and find -print0 for that, since a bare pipe splits those names apart.

Variations:

# Add a shared prefix to every match
for f in *.jpg; do
  mv -- "$f" "vacation-$f"
done

# Preview every rename before running it for real
for f in *report*; do
  echo mv -- "$f" "${f// /_}"
done

The preview variation replaces mv with echo mv, printing exactly what would run without touching any files. Read the output, and only then remove the echo and re-run it. ${f// /_} replaces every space in $f with an underscore, another parameter expansion rather than a regex substitution.

If the same rename needs to apply across subdirectories too, pair the loop with find instead of relying on the shell's own glob: find . -name "*.JPG" -print0 | while IFS= read -r -d '' f; do mv -- "$f" "${f%.JPG}.jpg"; done walks the whole tree safely, including filenames with spaces or newlines.