How to Create a Bash File Renamer

How to Create a Bash File Renamer

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

  • *.jpeg is a glob pattern expanded by Bash into a list of matching filenames.
  • [ -e "$file" ] || continue guards against the case where no files match the pattern (in which case Bash leaves the literal string *.jpeg unexpanded).
  • "${file%.jpeg}" uses Bash’s parameter expansion to strip the .jpeg suffix 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 get vacation_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 printf for 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 eval on 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/passwd could 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 of sed or awk to reduce process overhead.
  • Use find ... -print0 | while IFS= read -r -d '' file instead 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 sed or 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 mv documentation: 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 sed manual: https://www.gnu.org/software/sed/manual/sed.html
  • Bash Pattern Matching (Globbing): https://www.gnu.org/software/bash/manual/bash.html#Filename-Expansion
Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash Word Counter

How to Create a Bash Word Counter

Next Post
How to Create a Bash File Splitter

How to Create a Bash File Splitter

Related Posts