You know that feeling. You’ve watched 10 Bash tutorials. You can recite ls, cd, and grep in your sleep. But the moment someone asks you to actually build something, your brain goes blank.

Welcome to Tutorial Hell.

The escape route? Stop memorizing syntax. Start solving real problems.

Today, we’re building a File Organizer - a script that transforms your chaotic Downloads folder into a perfectly sorted collection. Images go to Images. PDFs go to Documents. No more digging through 200 random files to find that one screenshot.

By the end of this post, you’ll have a working tool you can use every day. Let’s go.

The Problem: Your Downloads Folder is a Disaster

Open your Downloads folder right now. Go ahead. I’ll wait.

If you’re like most developers, you’ll see:

  • Screenshot 2024-01-15.png mixed with resume_final_v3.pdf
  • random_video.mp4 sitting next to package.json
  • That ZIP file you downloaded 6 months ago and never opened

This chaos costs you time. Every. Single. Day.

Our script will:

  1. Scan a target directory for files
  2. Identify file types by their extension
  3. Create category folders (Images, Documents, Videos, etc.)
  4. Move each file to its proper home
  5. Handle duplicates gracefully
  6. Give you a summary report

The Toolkit: Commands Grouped by Purpose

Before we dive into code, let’s understand the tools we’ll use. Not as random commands, but as building blocks for our solution.

Group 1: Reconnaissance (Viewing Data)

CommandPurpose
lsList files in a directory
findSearch for files with specific criteria
basenameExtract filename from a path
statGet file information (size, dates)

Group 2: Logic and Processing

ConceptPurpose
${var##*.}Extract file extension
${var,,}Convert string to lowercase
declare -ACreate associative arrays (like dictionaries)
for/whileLoop through files
case/ifMake decisions

Group 3: Action and Output

CommandPurpose
mkdir -pCreate directories (including parents)
mvMove files
echoOutput messages
>>Append to log files

The Walkthrough: Building Step by Step

The fatal mistake most tutorials make: showing you the final 300-line script and saying “here’s how it works.”

We’re doing the opposite. We’ll build this iteratively, like you would in real life.

Step 1: List the Files

First, let’s just see what we’re working with:

# Create a test directory with sample files
mkdir -p test_folder
touch test_folder/{photo.jpg,document.pdf,video.mp4,song.mp3,data.json}

# List the files
ls test_folder

Output:

data.json  document.pdf  photo.jpg  song.mp3  video.mp4

Good. We have files. Now let’s process them one by one.

Step 2: Loop Through Files

for file in test_folder/*; do
    echo "Found: $file"
done

Output:

Found: test_folder/data.json
Found: test_folder/document.pdf
Found: test_folder/photo.jpg
Found: test_folder/song.mp3
Found: test_folder/video.mp4

We’re now touching every file. But we have the full path - we need just the filename.

Step 3: Extract the Filename and Extension

Here’s where Bash’s parameter expansion becomes your superpower:

for file in test_folder/*; do
    filename=$(basename "$file")
    extension="${filename##*.}"
    echo "File: $filename | Extension: $extension"
done

Output:

File: data.json | Extension: json
File: document.pdf | Extension: pdf
File: photo.jpg | Extension: jpg
File: song.mp3 | Extension: mp3
File: video.mp4 | Extension: mp4

What’s ${filename##*.} doing?

  • ## = Remove the longest match from the beginning
  • *. = Everything up to and including the last dot
  • Result = Just the extension

Step 4: Map Extensions to Categories

Now we need to know which folder each extension belongs to. Enter associative arrays:

# Define categories
declare -A CATEGORY_MAP
CATEGORY_MAP=(
    ["jpg"]="Images"
    ["png"]="Images"
    ["pdf"]="Documents"
    ["txt"]="Documents"
    ["mp4"]="Videos"
    ["mp3"]="Audio"
    ["json"]="Data"
)

# Get category for an extension
extension="jpg"
category="${CATEGORY_MAP[$extension]}"
echo "Extension '$extension' belongs to: $category"

Output:

Extension 'jpg' belongs to: Images

Step 5: Create Folders and Move Files

Let’s combine everything:

declare -A CATEGORY_MAP
CATEGORY_MAP=(
    ["jpg"]="Images"
    ["pdf"]="Documents"
    ["mp4"]="Videos"
    ["mp3"]="Audio"
    ["json"]="Data"
)

TARGET_DIR="test_folder"

for file in "$TARGET_DIR"/*; do
    # Skip if not a file
    [[ ! -f "$file" ]] && continue

    filename=$(basename "$file")
    extension="${filename##*.}"
    extension="${extension,,}"  # Convert to lowercase

    # Get category (default to "Others")
    category="${CATEGORY_MAP[$extension]:-Others}"

    # Create destination folder
    dest_dir="$TARGET_DIR/$category"
    mkdir -p "$dest_dir"

    # Move the file
    mv "$file" "$dest_dir/"
    echo "Moved: $filename -> $category/"
done

Output:

Moved: data.json -> Data/
Moved: document.pdf -> Documents/
Moved: photo.jpg -> Images/
Moved: song.mp3 -> Audio/
Moved: video.mp4 -> Videos/

Check the result:

ls -la test_folder/
Audio/
Data/
Documents/
Images/
Videos/

It works! But we’re not done. Real-world scripts need more.

Step 6: Handle Duplicates

What if photo.jpg already exists in the Images folder? We need to rename it:

get_unique_filename() {
    local dest_dir="$1"
    local filename="$2"
    local base="${filename%.*}"     # Everything before the last dot
    local ext="${filename##*.}"     # The extension
    local counter=1
    local new_filename="$filename"

    while [[ -e "${dest_dir}/${new_filename}" ]]; do
        new_filename="${base}_${counter}.${ext}"
        ((counter++))
    done

    echo "$new_filename"
}

# Usage
final_name=$(get_unique_filename "Images" "photo.jpg")
echo "$final_name"  # photo.jpg or photo_1.jpg if exists

Step 7: Add Dry-Run Mode

Never run a file-moving script without testing first. A dry-run flag lets you preview changes:

DRY_RUN=false

if [[ "$1" == "-d" || "$1" == "--dry-run" ]]; then
    DRY_RUN=true
    echo "[DRY-RUN MODE] No files will be moved"
fi

# Later, when moving files:
if [[ "$DRY_RUN" == true ]]; then
    echo "[DRY-RUN] Would move: $filename -> $category/"
else
    mv "$file" "$dest_dir/$final_name"
    echo "Moved: $filename -> $category/"
fi

Level Up: The Complete Script

Now let’s package everything into a proper, reusable script. Here’s the structure:

config.sh - Separate configuration for easy customization:

#!/bin/bash
# Configuration for File Organizer

declare -A CATEGORIES
CATEGORIES=(
    ["Images"]="jpg jpeg png gif bmp svg webp ico"
    ["Documents"]="pdf doc docx txt rtf odt xls xlsx ppt pptx"
    ["Videos"]="mp4 avi mkv mov wmv flv webm"
    ["Audio"]="mp3 wav flac aac ogg wma m4a"
    ["Archives"]="zip tar gz rar 7z bz2"
    ["Code"]="sh py js ts html css php java c cpp go rs"
    ["Data"]="json xml csv sql yaml yml toml"
)

DEFAULT_DIR="${HOME}/Downloads"
IGNORE_PATTERNS=".DS_Store Thumbs.db"
ENABLE_LOGGING=true
LOG_DIR="${HOME}/.local/log"

organize.sh - The main script:

#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/config.sh"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Options
DRY_RUN=false
VERBOSE=false

# Get category for a file extension
get_category() {
    local ext="${1,,}"
    for category in "${!CATEGORIES[@]}"; do
        for e in ${CATEGORIES[$category]}; do
            [[ "$e" == "$ext" ]] && echo "$category" && return
        done
    done
    echo "Others"
}

# Main logic
main() {
    local target_dir="${1:-$DEFAULT_DIR}"

    echo -e "${BLUE}[INFO]${NC} Organizing: $target_dir"

    for file in "$target_dir"/*; do
        [[ ! -f "$file" ]] && continue

        filename=$(basename "$file")
        extension="${filename##*.}"
        category=$(get_category "$extension")
        dest_dir="$target_dir/$category"

        mkdir -p "$dest_dir"
        mv "$file" "$dest_dir/"

        echo -e "${GREEN}[OK]${NC} $filename -> $category/"
    done

    echo -e "${GREEN}[SUCCESS]${NC} Organization complete!"
}

main "$@"

Make it executable and run:

chmod +x organize.sh config.sh
./organize.sh ~/Downloads

The Summary Report

A good script tells you what it did. Add tracking to your script:

declare -A MOVED_COUNT
TOTAL_MOVED=0

# After each move:
((TOTAL_MOVED++))
MOVED_COUNT[$category]=$((${MOVED_COUNT[$category]:-0} + 1))

# At the end, print summary:
echo "════════════════════════════════════════"
echo "         ORGANIZATION SUMMARY"
echo "════════════════════════════════════════"
printf "%-15s %s\n" "Category" "Files"
echo "────────────────────────────────────────"
for cat in "${!MOVED_COUNT[@]}"; do
    printf "%-15s %d\n" "$cat" "${MOVED_COUNT[$cat]}"
done
echo "────────────────────────────────────────"
printf "%-15s %d\n" "Total:" "$TOTAL_MOVED"

Output looks like:

════════════════════════════════════════
         ORGANIZATION SUMMARY
════════════════════════════════════════
Category        Files
────────────────────────────────────────
Images          12
Documents       8
Videos          3
Audio           5
Data            2
────────────────────────────────────────
Total:          30

What You Just Learned

You didn’t just copy-paste a script. You built it piece by piece. Along the way, you mastered:

  • Parameter expansion: ${var##*.}, ${var,,}, ${var%.*}
  • Associative arrays: declare -A for key-value mapping
  • Defensive programming: Dry-run mode, duplicate handling
  • Script structure: Separating config from logic
  • User feedback: Colors, summaries, logging

These aren’t isolated tricks. They’re patterns you’ll use in every Bash script you write.

Your Challenge

The script we built is functional, but there’s room to grow. Here are some challenges to level up:

  1. Add recursive mode: Process files in subdirectories too
  2. Add undo functionality: Keep a log that can reverse all moves
  3. Add file size stats: Show total size organized per category
  4. Add date-based organization: Organize by year/month instead of type
  5. Add custom rules: Let users define their own extension-to-category mappings via command line

Pick one. Build it. Break it. Fix it.

That’s how you escape Tutorial Hell for good.


Full source code available at: GitHub - File Organizer Project

Next in the series: Building a System Health Monitor in Bash