Skip to content

Project 02 — File Organizer

Build a script that automatically sorts a messy directory by file extension — with logging, dry-run mode, and undo support.

What You Will Build

A file organizer that: - Scans a directory for files and sorts them into subdirectories by type (images, music, documents, archives, etc.) - Logs every move with timestamp and original path - Supports --dry-run to preview changes without making them - Handles files with spaces, dots in names, and no extension - Can be run repeatedly without duplicating files

Project Files

File Purpose
organize.sh Main script
organize.conf Extension-to-folder mapping
organize.log Move history
undo.sh Reverses moves using the log file
README.md Usage documentation

Skills Covered

Associative arrays for extension mapping, for loops over files, parameter expansion for extensions, logging, dry-run pattern.

Getting Started

Step 1 — Extension Detection

for file in "$SOURCE_DIR"/*; do
    [[ -f "$file" ]] || continue
    ext="${file##*.}"
    ext="${ext,,}"      # lowercase
    echo "$file -> category: $ext"
done

Step 2 — Extension-to-Folder Mapping

declare -A FOLDERS
FOLDERS=(
    [jpg]="images"  [jpeg]="images"  [png]="images"  [gif]="images"  [webp]="images"
    [mp3]="music"   [wav]="music"    [flac]="music"  [aac]="music"
    [pdf]="documents" [docx]="documents" [xlsx]="documents" [txt]="documents"
    [zip]="archives" [tar]="archives" [gz]="archives" [7z]="archives"
    [mp4]="videos"  [mkv]="videos"  [avi]="videos"
    [sh]="scripts"  [py]="scripts"  [rb]="scripts"
)

Step 3 — The Full Organizer

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

SOURCE="${1:-.}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
LOGFILE="$SOURCE/organize.log"
moved=0

declare -A FOLDERS
# ... (mapping from Step 2) ...

for file in "$SOURCE"/*; do
    [[ -f "$file" ]] || continue
    filename="${file##*/}"
    ext="${filename##*.}"
    [[ "$filename" == "$ext" ]] && ext="no-extension"
    ext="${ext,,}"
    dest_dir="${FOLDERS[$ext]:-other}"

    if "$DRY_RUN"; then
        echo "[DRY RUN] $filename -> $dest_dir/"
    else
        mkdir -p "$SOURCE/$dest_dir"
        mv -- "$file" "$SOURCE/$dest_dir/$filename"
        echo "$(date '+%Y-%m-%d %H:%M:%S') MOVED $filename -> $dest_dir/" >> "$LOGFILE"
        (( moved++ ))
    fi
done

"$DRY_RUN" || echo "Moved $moved file(s). Log: $LOGFILE"

Step 4 — Undo Script

Read the log in reverse and move files back:

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

LOGFILE="${1:?Usage: $0 <logfile>}"
[[ -f "$LOGFILE" ]] || { echo "Log file not found"; exit 1; }

tac "$LOGFILE" | while IFS= read -r line; do
    filename=$(echo "$line" | awk '{print $3}')
    dest_dir=$(echo "$line" | awk '{print $5}' | tr -d '/')
    # Reverse the move
    echo "Restoring: $filename from $dest_dir/"
done

Stretch Goals

  • Add --watch mode using inotifywait to organize files as they appear
  • Add duplicate detection: if a file with the same name exists in the destination, append a counter
  • Generate an HTML summary report after organizing

[[01-system-health-monitor]] | [[03-log-analyzer]]