I can’t count how many times I’ve downloaded a folder full of photos named IMG_0001.jpg, IMG_0002.jpg, and so on, and wished they had meaningful names with dates or sequential numbering that actually made sense. Doing that by hand, one file at a time, is soul-crushing. That’s exactly why I built my own Bash file renamer, and over time I’ve refined it into a tool I now reach for constantly. Here’s everything I’ve learned about building one properly.
Why Use Bash for Batch Renaming
Renaming a handful of files manually is fine. Renaming hundreds or thousands is a different story. Bash gives you:
- Native looping and pattern matching (globbing) to target specific files.
- String manipulation tools (
sed, parameter expansion) to transform filenames. - Zero dependency on external renaming utilities — everything you need ships with core Linux tools.
- Easy integration into larger automation, like organizing downloaded files automatically.
The Core Command: mv
At its most basic, renaming a file in Bash is just:
mv old_name.txt new_name.txt
A file renamer script is essentially a loop that calls mv intelligently across many files based on some pattern or rule.
A Basic Batch Renamer
Let’s say I want to rename all .jpeg files to .jpg:
#!/bin/bash
for file in *.jpeg; do
[ -e "$file" ] || continue
mv -- "$file" "${file%.jpeg}.jpg"
done
echo "Renaming complete."
How This Works Internally
*.jpegis a glob pattern expanded by Bash into a list of matching filenames.[ -e "$file" ] || continueguards against the case where no files match the pattern (in which case Bash leaves the literal string*.jpegunexpanded)."${file%.jpeg}"uses Bash’s parameter expansion to strip the.jpegsuffix from the filename.mv -- "$file" ...performs the actual rename; the--protects against filenames that start with a dash being misinterpreted as options.
Adding Sequential Numbering
Here’s a script I use constantly for renaming a batch of photos into a clean sequential format:
#!/bin/bash
set -euo pipefail
prefix="vacation_photo"
counter=1
for file in *.jpg; do
[ -e "$file" ] || continue
new_name=$(printf "%s_%03d.jpg" "$prefix" "$counter")
mv -- "$file" "$new_name"
echo "Renamed $file -> $new_name"
((counter++))
done
Breaking It Down
printf "%s_%03d.jpg"formats the counter with zero-padding, so you getvacation_photo_001.jpg,vacation_photo_002.jpg, etc., which sort correctly.((counter++))increments the counter using Bash arithmetic expansion.- Files are processed in the order returned by the shell’s glob expansion, which is alphabetical by default — if you need a specific order (like by modification date), you’ll want to sort explicitly (shown below).
Renaming Based on File Modification Date
Sometimes you want filenames to reflect metadata like creation or modification time, which is great for organizing camera exports or log files.
#!/bin/bash
set -euo pipefail
for file in *.jpg; do
[ -e "$file" ] || continue
timestamp=$(date -r "$file" +"%Y%m%d_%H%M%S")
mv -- "$file" "${timestamp}.jpg"
done
Here, date -r "$file" reads the file’s last modification time and formats it into a sortable timestamp string, which then becomes the new filename.
Using sed for Pattern-Based Renaming
If you need more advanced text substitution — like replacing spaces with underscores or stripping unwanted characters — sed combined with a loop works well:
#!/bin/bash
set -euo pipefail
for file in *; do
[ -f "$file" ] || continue
new_name=$(echo "$file" | sed -e 's/ /_/g' -e 's/[^A-Za-z0-9._-]//g')
if [ "$file" != "$new_name" ]; then
mv -- "$file" "$new_name"
echo "Renamed: $file -> $new_name"
fi
done
This replaces spaces with underscores and strips out any character that isn’t alphanumeric, a period, underscore, or hyphen — handy for cleaning up messy filenames before uploading them somewhere strict about naming conventions.
A Dry-Run Mode for Safety
I never trust a renaming script fully until I’ve tested it. Adding a dry-run flag has saved me from disaster more than once:
#!/bin/bash
set -euo pipefail
dry_run=false
if [[ "${1:-}" == "--dry-run" ]]; then
dry_run=true
fi
for file in *.txt; do
[ -e "$file" ] || continue
new_name="renamed_${file}"
if [ "$dry_run" = true ]; then
echo "[DRY RUN] Would rename: $file -> $new_name"
else
mv -- "$file" "$new_name"
echo "Renamed: $file -> $new_name"
fi
done
Running ./renamer.sh --dry-run first lets you preview every change before committing to it.
Real-World Use Cases
- Photo and media organization: Renaming camera dumps (
IMG_1234.jpg) into meaningful, sortable names based on date or event. - Log rotation cleanup: Standardizing log filenames across servers that generate inconsistent naming schemes.
- Batch file preparation: Normalizing filenames (removing spaces, special characters) before uploading to systems with strict naming rules.
- Data pipeline staging: Renaming incoming data files with a consistent prefix and timestamp so downstream processes can find them predictably.
- Deduplication prep: Renaming files consistently before running deduplication tools that rely on filename patterns.
Automation Example
Here’s a renamer I run via cron to standardize files dropped into a shared folder every night:
#!/bin/bash
set -euo pipefail
target_dir="/data/incoming"
log_file="/var/log/renamer.log"
cd "$target_dir"
for file in *; do
[ -f "$file" ] || continue
clean_name=$(echo "$file" | tr ' ' '_' | tr -cd 'A-Za-z0-9._-')
if [ "$file" != "$clean_name" ] && [ -n "$clean_name" ]; then
mv -- "$file" "$clean_name"
echo "$(date '+%F %T') Renamed '$file' -> '$clean_name'" >> "$log_file"
fi
done
This keeps a shared directory tidy automatically, logging every rename for auditing purposes.
Best Practices
- Always test with a dry-run mode before executing renames on important data.
- Use
mv --to protect against filenames that begin with a dash. - Quote all variables (
"$file") to handle filenames containing spaces correctly. - Use
printffor zero-padded sequential numbering to keep files sorted correctly. - Log every rename operation when running in automated/unattended mode, so changes can be audited or reversed.
- Check for filename collisions before renaming to avoid accidentally overwriting an existing file.
Security Considerations
- Command injection via filenames: Never use unquoted variables or
evalon filenames, especially if the filenames come from an untrusted source (like an upload folder) — malicious filenames can otherwise be crafted to inject shell commands. - Path traversal: Sanitize filenames if they originate from external, untrusted input (like a network upload), since a name like
../../etc/passwdcould otherwise cause unintended file writes. - Accidental overwrites: Use
mv -n(no-clobber) if you want to guarantee an existing file is never silently overwritten during a rename. - Permissions: Batch renaming doesn’t change file permissions, but always ensure your script runs with the minimum privileges necessary — avoid running renamers as root unless absolutely required.
Optimization Tips
- For extremely large directories (tens of thousands of files), avoid spawning a new subshell (
$(...)) per file if possible; where feasible, use built-in string manipulation (parameter expansion) instead ofsedorawkto reduce process overhead. - Use
find ... -print0 | while IFS= read -r -d '' fileinstead of a plain glob when working with deeply nested directories or filenames containing newlines. - Batch operations in a single pass rather than multiple sequential scripts scanning the same directory repeatedly.
Troubleshooting Common Issues
Problem: “No such file or directory” errors during renaming. This usually means the glob pattern didn’t match any files, and the literal pattern string was passed to mv. Always guard loops with [ -e "$file" ] || continue.
Problem: Filenames with spaces break the script. Ensure every variable reference is quoted ("$file", not $file).
Problem: Some files get skipped. Check whether your glob pattern is case-sensitive — *.JPG and *.jpg won’t match the same files unless you enable shopt -s nocaseglob.
Problem: Script renames files in the wrong order. Bash’s default glob expansion is alphabetical. If you need chronological order, sort explicitly using find ... -printf '%T@ %p\n' | sort -n.
Common Mistakes to Avoid
- Forgetting to quote filenames, which breaks on spaces or special characters.
- Not checking for existing files with the same target name, causing accidental data loss.
- Running renaming scripts on important data without a dry-run test first.
- Using
sedor regex renaming rules that are too aggressive and unintentionally strip meaningful characters.
Frequently Asked Questions
Can I undo a batch rename if something goes wrong? Only if you logged the original-to-new filename mapping beforehand. I recommend always writing a log file of renames so you can reverse them if needed.
Does Bash have a built-in rename command? Not natively — rename is a separate utility (often Perl-based) available on many distributions, but writing your own loop with mv gives you full control without depending on it.
How do I rename files recursively through subdirectories? Use find /path -type f -name "*.jpg" -exec bash -c 'mv "$1" "${1%.jpg}.jpeg"' _ {} \; or loop over find ... -print0 output.
Will this work on macOS as well as Linux? Mostly yes, though some commands like date and sed have different flag behavior on BSD-based macOS versus GNU/Linux — test carefully or install GNU coreutils via Homebrew for consistency.
How do I handle filenames with Unicode characters? Make sure your terminal and script locale are set to UTF-8 (export LC_ALL=en_US.UTF-8) to avoid corrupting non-ASCII filenames during renaming.
Summary
A Bash file renamer starts as a simple loop around mv, but it becomes genuinely powerful once you add sequential numbering, timestamp-based naming, pattern cleanup with sed, and a dry-run safety mode. The biggest lessons I’ve learned: always quote your variables, always test with a dry run first, and always log what you renamed so you can undo it if something goes wrong.
References
- GNU Coreutils
mvdocumentation: https://www.gnu.org/software/coreutils/manual/html_node/mv-invocation.html - Bash Reference Manual (Parameter Expansion): https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion
- GNU
sedmanual: https://www.gnu.org/software/sed/manual/sed.html - Bash Pattern Matching (Globbing): https://www.gnu.org/software/bash/manual/bash.html#Filename-Expansion
