Shore Up
a heap of loose papers being sorted by hand into labelled folders arranged by month and year in an open drawer
Linux

Bulk-Rename and Reorganise Files by Date with Bash

Ketan Aagja8 min read
No ratings yet

I keep ending up with directories that are one flat dumping ground — exported reports, scans, camera dumps — hundreds of files with no order. This guide builds a small bash script that reads each file's modification date, renames it with a YYYY-MM-DD_ prefix, and moves it into DEST/YYYY/MM/ folders. It is safe by default: it prints what it would do and moves nothing until you explicitly tell it to.

Before you run this

What it does: for every regular file directly inside a source directory, the script reads the file's last-modification date, prepends that date to the filename, and moves the file into a year/month folder tree under a destination directory. Its purpose is to turn one flat folder into a dated, browsable archive.

Privileges: none special. Run it as your normal user. You only need standard read/write permission on both the source and destination directories. If the files are owned by root or another user, you'll need sudo, but for your own files you should not — and if you catch yourself reaching for sudo here, stop and check why the files aren't yours first.

This script MOVES files and RENAMES them. A move plus rename is not trivially reversible: after it runs, the original filenames and locations are gone. That is why the script defaults to a dry run and why the real run writes a log of every move so you can reverse it. Read the whole script before running it. Try it first on a copy of a folder, or on a throwaway directory with a handful of test files — never point it straight at your only copy of anything.

One more caveat: this sorts by modification time, not by any "date taken" stored inside the file. If you edited a photo last week, its modification time is last week, not the day you shot it. For photos where you want the capture date, that metadata lives in EXIF and needs a dedicated tool (exiftool), which is out of scope here — I mention it so you don't misuse modification time for that job.

Assumptions

  • Debian or Ubuntu, a recent stable release, with the default bash shell and GNU coreutils (date, mv, mkdir, basename). These are all part of a base install.
  • The GNU date -r FILE form is what reads a file's modification time here. On macOS or BSD the flags differ; this is written for GNU coreutils.
  • Files to sort sit directly inside the source directory. This version is deliberately not recursive — that keeps the behaviour predictable. I note below how to make it recursive if you truly want that.

The script

Save this as organise-by-date.sh.

#!/usr/bin/env bash
set -euo pipefail

# Usage: organise-by-date.sh SOURCE_DIR DEST_DIR
# Dry-run by default. Set DRYRUN=0 to actually move files.

SRC="${1:?Usage: organise-by-date.sh SOURCE_DIR DEST_DIR}"
DEST="${2:?Usage: organise-by-date.sh SOURCE_DIR DEST_DIR}"
DRYRUN="${DRYRUN:-1}"                 # 1 = show only; 0 = perform moves
LOG="${LOG:-organise-$(date +%Y%m%d-%H%M%S).log}"

shopt -s nullglob                     # if no files match, loop runs zero times

for file in "$SRC"/*; do
    [ -f "$file" ] || continue        # skip directories and non-regular files

    # Modification date of the file (GNU coreutils: -r reads the file's mtime)
    moddate=$(date -r "$file" +%Y-%m-%d)
    year=$(date -r "$file" +%Y)
    month=$(date -r "$file" +%m)

    base=$(basename "$file")
    target_dir="$DEST/$year/$month"
    target="$target_dir/${moddate}_${base}"

    if [ "$DRYRUN" = "1" ]; then
        printf 'DRY-RUN: %s  ->  %s\n' "$file" "$target"
    else
        mkdir -p "$target_dir"
        # -n never overwrites an existing target; -v prints each move
        mv -n -v "$file" "$target"
        # Record the move (old TAB new) so it can be reversed later
        printf '%s\t%s\n' "$file" "$target" >> "$LOG"
    fi
done

Make it executable:

chmod +x organise-by-date.sh

Run the dry run first

Point it at your source and a destination. Replace /path/to/source and /path/to/archive with your real paths — they are placeholders.

./organise-by-date.sh /path/to/source /path/to/archive

You'll get a list of DRY-RUN: lines showing every intended move, and nothing on disk changes. Read that list. Check the dates look right and the target tree is what you expect. This is the step that catches mistakes — a wrong source path, files whose modification dates aren't what you assumed — while they're still cheap to fix.

Do it for real

When the dry run looks correct, set DRYRUN=0:

DRYRUN=0 ./organise-by-date.sh /path/to/source /path/to/archive

Now it creates the year/month folders, moves each file, and writes a log file named like organise-20240115-093000.log in your current directory. Keep that log — it's your undo trail.

A note on mv -n: if a target filename already exists (say two files with the same name and the same modification date), mv -n will not overwrite it and will leave the original in place. Those files simply stay in the source. That's the safe outcome; just be aware some files may remain behind for that reason.

If you want it recursive

I kept this non-recursive on purpose. If you genuinely need to walk subdirectories, the standard tool is find, iterated safely with -print0 and a while read -d '' loop rather than a for over glob output. That's a different pattern with its own quoting pitfalls — treat it as a separate change, test it on a copy, and don't bolt it on blindly.

Verify it worked

Check the destination tree took shape:

# See the folder structure that was created
find /path/to/archive -maxdepth 2 -type d | sort

# Count files that landed in a given month
ls -la /path/to/archive/2024/01/

Confirm the source is now empty (or holds only the files mv -n deliberately skipped):

ls -la /path/to/source/

Spot-check that a file's new date prefix matches its modification time:

stat -c '%y  %n' /path/to/archive/2024/01/2024-01-15_report.pdf

The date in the filename should match the date in the stat output.

How to undo it

Because the real run logged every move as old<TAB>new, you can reverse it. Test this on your own log first, and note it recreates original paths only if the source directories still exist — recreate them with mkdir -p if you deleted anything.

# Reverse the moves recorded in a log, newest run's log named here.
# Reads each line, splits on the tab, and moves the file back.
while IFS=$'\t' read -r old new; do
    mkdir -p "$(dirname "$old")"      # ensure the original folder exists
    mv -n -v "$new" "$old"            # -n so we never clobber anything
done < organise-20240115-093000.log

Replace the log filename with your actual one. After it runs, ls your source directory to confirm the files are back, and check the archive folders are empty before removing the now-unused year/month directories by hand.

If you're ever unsure, the honest safe move is the oldest one in the book: cp your source directory to a backup before you start, and only delete the backup once you've confirmed the result. For the exact behaviour of any flag here, man date, man mv, and man stat on your own machine are the authoritative reference — the GNU coreutils manuals describe date -r and mv -n precisely.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.