Bulk-rename Files Safely on Linux
- Pure shell: a
forloop 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.
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.
rename commandsThere 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
prenameand use that, or checkrename --help— if it mentions Perl, you're good; if it mentions "from to file", that's util-linux. - Arch: the AUR has
perl-renameif 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.
#!/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 -nrefuses to overwrite, but the second rename will silently fail.mv -iprompts.mvalone overwrites without asking; don't use it for batch renames. - Running the wrong
rename. See the box above. The util-linuxrenamedoesn'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--tomvbefore the arguments, and preferfind -print0piped intoxargs -0when 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
sudoor fix the ownership; see the file permissions reference.
Related reading
- Essential Linux commands — reference for
mv,forloops, parameter expansion. - File permissions — when a rename fails with EACCES.
- Find the largest files — sometimes "rename these" turns into "delete these" once you see the sizes.
- Back to all scripts