Bulk-rename Files Safely on Linux

In short
  • Pure shell: a for loop with parameter expansion. Works everywhere, zero dependencies.
  • rename (Perl): regex-driven, the most flexible option, but check which version you have first.
  • mmv: wildcard-based, very readable for "shift this column to that column" patterns.
  • Every approach has a dry-run mode. Always use it before the real run.

Why this is worth doing carefully

Renaming a thousand files by hand is unbearable. Renaming them with a one-line command and an off-by-one in the regex is worse: by the time you notice, you've overwritten files with each other and there's no obvious way back. The recipes below all have a dry-run step. Use it.

Read before you run

If two files end up with the same target name, the second rename will overwrite the first. mv with -i or -n prevents this; the recipes below include those flags. Always do a dry run on a copy of the directory the first time you try a pattern.

1. Pure shell: a for loop

The most portable approach uses just mv and Bash parameter expansion. No extra packages, works on every distribution.

Example: replace spaces with underscores

# Dry run: print what would happen
for f in *.jpg; do
  new="${f// /_}"
  [ "$f" = "$new" ] || echo "mv -- '$f' '$new'"
done

# Do it for real
for f in *.jpg; do
  new="${f// /_}"
  [ "$f" = "$new" ] || mv -n -- "$f" "$new"
done

The ${f// /_} form does global substitution inside the variable. ${f// /_} = "replace every space with an underscore". For a single substitution use ${f/ /_}. The double-dash -- after mv stops mv from misinterpreting a filename that starts with - as a flag.

Example: change extension

# Rename *.jpeg to *.jpg
for f in *.jpeg; do
  mv -n -- "$f" "${f%.jpeg}.jpg"
done

${f%.jpeg} strips the suffix; appending .jpg gives the new name.

Example: add a prefix

# Prefix every PDF with the current date
prefix="$(date +%Y-%m-%d)_"
for f in *.pdf; do
  mv -n -- "$f" "${prefix}${f}"
done

Example: number sequentially

# Rename images to photo-001.jpg, photo-002.jpg, ...
i=1
for f in *.jpg; do
  printf -v new "photo-%03d.jpg" "$i"
  mv -n -- "$f" "$new"
  i=$((i + 1))
done

printf -v writes formatted output into a variable rather than printing it; %03d pads numbers with leading zeros to three digits so sorting stays correct.

2. The rename utility

rename takes a Perl regex and applies it to every filename you pass. It's the right tool when the substitution is more complex than parameter expansion can express cleanly.

Two different rename commands

There are two different programs called rename floating around. The Perl one (often called prename on Red Hat-family systems) takes a regex. The util-linux one (default on some distributions) takes plain string substitution. The Perl one is what most online examples assume.

  • Debian / Ubuntu: the Perl one is the default rename. You're fine.
  • Fedora / RHEL: install prename and use that, or check rename --help — if it mentions Perl, you're good; if it mentions "from to file", that's util-linux.
  • Arch: the AUR has perl-rename if you want the Perl behaviour.

Examples (Perl-style rename)

# Dry run with -n; real run without it
rename -n 's/\.jpeg$/.jpg/' *.jpeg
rename    's/\.jpeg$/.jpg/' *.jpeg

# Lowercase every filename in the current directory
rename -n 'y/A-Z/a-z/' *

# Replace spaces with underscores
rename -n 's/ /_/g' *

# Remove a "copy of " prefix
rename -n 's/^copy of //i' *

# Add a directory-name prefix to every file inside that directory
rename -n 's:^:photos-:' photos/*.jpg

# Insert a numeric counter as the filename advances
# (uses Perl's $i scalar; the regex runs once per file)
ls *.txt | rename -n 'our $i; $_ = sprintf("doc-%03d.txt", ++$i)'

The -n flag prints what would happen without doing it. -v prints what's happening as it happens. The two together are useful: rename -nv ....

3. mmv for "shift this to that" patterns

mmv ("multiple move") uses wildcards and numbered references. It's the most readable option when the rename pattern is "everything before X becomes Y" rather than a regex.

Install it with your package manager (mmv on every distribution that has it). It's not preinstalled, so if you don't already have it, the shell loop or rename approaches above are usually enough.

# Dry run with -n
mmv -n '*.jpeg' '#1.jpg'
mmv    '*.jpeg' '#1.jpg'

# Reorder parts of a name: "Artist - Title.mp3" -> "Title - Artist.mp3"
mmv -n '* - *.mp3' '#2 - #1.mp3'

# Strip a fixed prefix
mmv -n 'IMG_*.jpg' '#1.jpg'

Each * in the "from" pattern corresponds to #1, #2, etc. in the "to" pattern, in order. The grammar is small, but for the common "shuffle columns" task it's more legible than the regex equivalent.

A safe wrapper script

If you want a single script you can keep in ~/bin that always does a dry run first and asks for confirmation, the wrapper below uses the Perl-style rename under the hood.

safe-rename.sh
#!/usr/bin/env bash
#
# safe-rename.sh — preview a rename, then ask before doing it for real.
#
# Usage:
#   ./safe-rename.sh 's/PATTERN/REPLACEMENT/FLAGS' files...
#
# Example:
#   ./safe-rename.sh 's/\.jpeg$/.jpg/' *.jpeg
#
set -euo pipefail

if [ "$#" -lt 2 ]; then
  echo "Usage: $0 's/pattern/replacement/flags' files..." >&2
  exit 2
fi

EXPR="$1"; shift

# Use whichever rename command is the Perl one.
if command -v prename >/dev/null 2>&1; then
  RENAME="prename"
else
  RENAME="rename"
fi

echo "--- Dry run ---"
"$RENAME" -n "$EXPR" "$@"

echo
read -r -p "Apply these renames for real? [y/N] " yn
case "$yn" in
  y|Y|yes|YES)
    "$RENAME" -v "$EXPR" "$@"
    ;;
  *)
    echo "Cancelled."
    exit 0
    ;;
esac

Common mistakes

  • Forgetting the dry run. Every example above starts with one. Use it. Renames are not always reversible.
  • Collisions. If two different files would end up with the same target name, you lose one. mv -n refuses to overwrite, but the second rename will silently fail. mv -i prompts. mv alone overwrites without asking; don't use it for batch renames.
  • Running the wrong rename. See the box above. The util-linux rename doesn't take regexes; if you paste a regex example into it, the result is rarely what you want.
  • Filenames with newlines, leading dashes, or shell metacharacters. Quote your variables ("$f"), pass -- to mv before the arguments, and prefer find -print0 piped into xargs -0 when filenames might contain anything unusual.
  • Renaming files you don't own. If the loop traverses a directory tree owned by root, you'll get a flurry of "permission denied" errors. Either run with sudo or fix the ownership; see the file permissions reference.

Related reading