Utility Scripts

This section contains practical utility scripts for everyday tasks, system maintenance, and productivity enhancement.

File and Directory Utilities

Duplicate File Finder

#!/bin/bash
# find_duplicates.sh - Find and manage duplicate files

SEARCH_DIR="${1:-.}"
ACTION="${ACTION:-list}"
MIN_SIZE="${MIN_SIZE:-1}"
HASH_ALGORITHM="${HASH_ALGORITHM:-md5sum}"

# Temporary files
TEMP_DIR="/tmp/duplicate_finder.$$"
mkdir -p "$TEMP_DIR"

# Cleanup function
cleanup() {
    rm -rf "$TEMP_DIR"
}
trap cleanup EXIT

# Logging function
log() {
    echo "$(date '+%H:%M:%S') - $*"
}

# Calculate file hash
calculate_hash() {
    local file="$1"
    case "$HASH_ALGORITHM" in
        md5sum) md5sum "$file" | cut -d' ' -f1 ;;
        sha1sum) sha1sum "$file" | cut -d' ' -f1 ;;
        sha256sum) sha256sum "$file" | cut -d' ' -f1 ;;
        *) md5sum "$file" | cut -d' ' -f1 ;;
    esac
}

# Find files by size
find_by_size() {
    log "Finding files by size..."

    find "$SEARCH_DIR" -type f -size +${MIN_SIZE}c -exec stat -c "%s %n" {} \; | \
    sort -n > "$TEMP_DIR/files_by_size.txt"

    # Group files by size
    awk '{print $1}' "$TEMP_DIR/files_by_size.txt" | uniq -d > "$TEMP_DIR/duplicate_sizes.txt"

    local duplicate_size_count=$(wc -l < "$TEMP_DIR/duplicate_sizes.txt")
    log "Found $duplicate_size_count different file sizes with potential duplicates"
}

# Find duplicates by hash
find_duplicates() {
    log "Calculating file hashes..."

    local processed=0
    local total=$(wc -l < "$TEMP_DIR/duplicate_sizes.txt")

    while IFS= read -r size; do
        ((processed++))
        echo -ne "\rProgress: $processed/$total"

        # Get all files of this size
        grep "^$size " "$TEMP_DIR/files_by_size.txt" | cut -d' ' -f2- | while IFS= read -r file; do
            if [ -f "$file" ]; then
                local hash=$(calculate_hash "$file")
                echo "$hash $size $file"
            fi
        done >> "$TEMP_DIR/files_with_hash.txt"
    done < "$TEMP_DIR/duplicate_sizes.txt"

    echo

    # Group by hash
    sort "$TEMP_DIR/files_with_hash.txt" > "$TEMP_DIR/sorted_hashes.txt"
    awk '{print $1}' "$TEMP_DIR/sorted_hashes.txt" | uniq -d > "$TEMP_DIR/duplicate_hashes.txt"

    local duplicate_hash_count=$(wc -l < "$TEMP_DIR/duplicate_hashes.txt")
    log "Found $duplicate_hash_count groups of duplicate files"
}

# List duplicate files
list_duplicates() {
    log "Listing duplicate files..."

    local group_num=0
    local total_duplicates=0
    local total_wasted_space=0

    while IFS= read -r hash; do
        ((group_num++))
        echo
        echo "Duplicate Group $group_num (Hash: $hash):"
        echo "=========================================="

        local files=()
        local file_count=0
        local file_size=0

        grep "^$hash " "$TEMP_DIR/sorted_hashes.txt" | while IFS=' ' read -r h size file; do
            ((file_count++))
            file_size="$size"
            echo "$file_count. $file ($(numfmt --to=iec-i --suffix=B $size))"
            files+=("$file")
        done

        # Calculate wasted space (all but one file)
        local wasted=$((file_size * (file_count - 1)))
        total_wasted_space=$((total_wasted_space + wasted))
        total_duplicates=$((total_duplicates + file_count - 1))

        echo "Files in group: $file_count"
        echo "Wasted space: $(numfmt --to=iec-i --suffix=B $wasted)"

    done < "$TEMP_DIR/duplicate_hashes.txt"

    echo
    echo "Summary:"
    echo "========"
    echo "Duplicate groups: $group_num"
    echo "Total duplicate files: $total_duplicates"
    echo "Total wasted space: $(numfmt --to=iec-i --suffix=B $total_wasted_space)"
}

# Interactive duplicate removal
interactive_remove() {
    log "Starting interactive duplicate removal..."

    local group_num=0
    local removed_count=0
    local freed_space=0

    while IFS= read -r hash; do
        ((group_num++))
        echo
        echo "Duplicate Group $group_num (Hash: $hash):"
        echo "=========================================="

        local files=()
        local file_count=0
        local file_size=0

        # Collect files in this group
        while IFS=' ' read -r h size file; do
            if [ "$h" = "$hash" ]; then
                ((file_count++))
                file_size="$size"
                files+=("$file")
                echo "$file_count. $file"
                echo "   Size: $(numfmt --to=iec-i --suffix=B $size)"
                echo "   Modified: $(stat -c %y "$file")"
            fi
        done < "$TEMP_DIR/sorted_hashes.txt"

        echo
        echo "Choose action:"
        echo "k) Keep all files"
        echo "1-$file_count) Keep only file number X (delete others)"
        echo "a) Auto-keep oldest file"
        echo "s) Skip this group"

        read -p "Your choice: " choice

        case "$choice" in
            k|K)
                echo "Keeping all files in this group"
                ;;
            [1-9]*)
                if [ "$choice" -ge 1 ] && [ "$choice" -le $file_count ]; then
                    local keep_index=$((choice - 1))
                    echo "Keeping: ${files[$keep_index]}"

                    for i in "${!files[@]}"; do
                        if [ $i -ne $keep_index ]; then
                            echo "Removing: ${files[$i]}"
                            if rm "${files[$i]}"; then
                                ((removed_count++))
                                freed_space=$((freed_space + file_size))
                            fi
                        fi
                    done
                else
                    echo "Invalid choice, skipping group"
                fi
                ;;
            a|A)
                # Keep oldest file
                local oldest_file=""
                local oldest_time=0

                for file in "${files[@]}"; do
                    local mtime=$(stat -c %Y "$file")
                    if [ $oldest_time -eq 0 ] || [ $mtime -lt $oldest_time ]; then
                        oldest_time=$mtime
                        oldest_file="$file"
                    fi
                done

                echo "Auto-keeping oldest file: $oldest_file"

                for file in "${files[@]}"; do
                    if [ "$file" != "$oldest_file" ]; then
                        echo "Removing: $file"
                        if rm "$file"; then
                            ((removed_count++))
                            freed_space=$((freed_space + file_size))
                        fi
                    fi
                done
                ;;
            s|S)
                echo "Skipping this group"
                ;;
            *)
                echo "Invalid choice, skipping group"
                ;;
        esac

    done < "$TEMP_DIR/duplicate_hashes.txt"

    echo
    echo "Removal Summary:"
    echo "==============="
    echo "Files removed: $removed_count"
    echo "Space freed: $(numfmt --to=iec-i --suffix=B $freed_space)"
}

# Auto-remove duplicates (keep oldest)
auto_remove() {
    log "Auto-removing duplicates (keeping oldest files)..."

    local removed_count=0
    local freed_space=0

    while IFS= read -r hash; do
        local files=()
        local file_size=0

        # Collect files in this group
        while IFS=' ' read -r h size file; do
            if [ "$h" = "$hash" ]; then
                files+=("$file")
                file_size="$size"
            fi
        done < "$TEMP_DIR/sorted_hashes.txt"

        # Find oldest file
        local oldest_file=""
        local oldest_time=0

        for file in "${files[@]}"; do
            local mtime=$(stat -c %Y "$file")
            if [ $oldest_time -eq 0 ] || [ $mtime -lt $oldest_time ]; then
                oldest_time=$mtime
                oldest_file="$file"
            fi
        done

        echo "Keeping oldest: $oldest_file"

        # Remove other files
        for file in "${files[@]}"; do
            if [ "$file" != "$oldest_file" ]; then
                echo "Removing: $file"
                if rm "$file"; then
                    ((removed_count++))
                    freed_space=$((freed_space + file_size))
                fi
            fi
        done

    done < "$TEMP_DIR/duplicate_hashes.txt"

    echo
    echo "Auto-removal Summary:"
    echo "===================="
    echo "Files removed: $removed_count"
    echo "Space freed: $(numfmt --to=iec-i --suffix=B $freed_space)"
}

# Generate report
generate_report() {
    local report_file="duplicate_report_$(date +%Y%m%d_%H%M%S).txt"

    {
        echo "Duplicate Files Report"
        echo "====================="
        echo "Generated: $(date)"
        echo "Search directory: $SEARCH_DIR"
        echo "Minimum file size: $MIN_SIZE bytes"
        echo "Hash algorithm: $HASH_ALGORITHM"
        echo

        list_duplicates

    } > "$report_file"

    log "Report saved: $report_file"
}

# Main execution
main() {
    if [ ! -d "$SEARCH_DIR" ]; then
        echo "Error: Directory '$SEARCH_DIR' does not exist"
        exit 1
    fi

    log "Starting duplicate file search in: $SEARCH_DIR"
    log "Minimum file size: $MIN_SIZE bytes"
    log "Hash algorithm: $HASH_ALGORITHM"

    find_by_size
    find_duplicates

    case "$ACTION" in
        list)
            list_duplicates
            ;;
        interactive)
            interactive_remove
            ;;
        auto)
            auto_remove
            ;;
        report)
            generate_report
            ;;
        *)
            echo "Unknown action: $ACTION"
            echo "Available actions: list, interactive, auto, report"
            exit 1
            ;;
    esac
}

# Show usage
show_usage() {
    cat << EOF
Duplicate File Finder - Find and manage duplicate files

Usage: $0 [directory] [options]

Options:
  -a, --action ACTION     Action to perform: list, interactive, auto, report
  -s, --min-size SIZE     Minimum file size in bytes (default: 1)
  -h, --hash ALGORITHM    Hash algorithm: md5sum, sha1sum, sha256sum
  --help                  Show this help

Environment Variables:
  ACTION                  Action to perform
  MIN_SIZE               Minimum file size
  HASH_ALGORITHM         Hash algorithm

Actions:
  list                   List duplicate files (default)
  interactive            Interactive duplicate removal
  auto                   Auto-remove duplicates (keep oldest)
  report                 Generate detailed report

Examples:
  $0 /home/user/Documents
  $0 /home/user/Pictures --action interactive
  ACTION=auto MIN_SIZE=1024 $0 /var/log
EOF
}

# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -a|--action)
            ACTION="$2"
            shift 2
            ;;
        -s|--min-size)
            MIN_SIZE="$2"
            shift 2
            ;;
        -h|--hash)
            HASH_ALGORITHM="$2"
            shift 2
            ;;
        --help)
            show_usage
            exit 0
            ;;
        -*)
            echo "Unknown option: $1"
            show_usage
            exit 1
            ;;
        *)
            if [ -z "$SEARCH_DIR" ] || [ "$SEARCH_DIR" = "." ]; then
                SEARCH_DIR="$1"
            fi
            shift
            ;;
    esac
done

main

Directory Synchronizer

#!/bin/bash
# sync_directories.sh - Synchronize directories with various options

SOURCE_DIR="$1"
TARGET_DIR="$2"
SYNC_MODE="${SYNC_MODE:-mirror}"
DRY_RUN="${DRY_RUN:-false}"
EXCLUDE_FILE="${EXCLUDE_FILE:-}"
LOG_FILE="${LOG_FILE:-sync_$(date +%Y%m%d_%H%M%S).log}"

# Sync statistics
declare -i files_copied=0
declare -i files_updated=0
declare -i files_deleted=0
declare -i dirs_created=0
declare -i bytes_transferred=0

# Logging function
log() {
    local level="$1"
    shift
    local message="$*"
    echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message" | tee -a "$LOG_FILE"
}

# Check if file should be excluded
is_excluded() {
    local file="$1"

    if [ -n "$EXCLUDE_FILE" ] && [ -f "$EXCLUDE_FILE" ]; then
        while IFS= read -r pattern; do
            # Skip comments and empty lines
            [[ $pattern =~ ^[[:space:]]*# ]] && continue
            [[ -z $pattern ]] && continue

            if [[ $file =~ $pattern ]]; then
                return 0  # File is excluded
            fi
        done < "$EXCLUDE_FILE"
    fi

    return 1  # File is not excluded
}

# Copy file with verification
copy_file() {
    local src="$1"
    local dst="$2"
    local action="$3"  # "copy" or "update"

    if [ "$DRY_RUN" = "true" ]; then
        log INFO "DRY RUN: Would $action $src -> $dst"
        return 0
    fi

    # Create destination directory if needed
    local dst_dir=$(dirname "$dst")
    if [ ! -d "$dst_dir" ]; then
        if mkdir -p "$dst_dir"; then
            log INFO "Created directory: $dst_dir"
            ((dirs_created++))
        else
            log ERROR "Failed to create directory: $dst_dir"
            return 1
        fi
    fi

    # Copy file
    if cp -p "$src" "$dst"; then
        local file_size=$(stat -c%s "$src" 2>/dev/null || stat -f%z "$src" 2>/dev/null)
        bytes_transferred=$((bytes_transferred + file_size))

        if [ "$action" = "copy" ]; then
            log INFO "Copied: $src -> $dst ($(numfmt --to=iec-i --suffix=B $file_size))"
            ((files_copied++))
        else
            log INFO "Updated: $src -> $dst ($(numfmt --to=iec-i --suffix=B $file_size))"
            ((files_updated++))
        fi
        return 0
    else
        log ERROR "Failed to $action: $src -> $dst"
        return 1
    fi
}

# Delete file or directory
delete_item() {
    local item="$1"

    if [ "$DRY_RUN" = "true" ]; then
        log INFO "DRY RUN: Would delete $item"
        return 0
    fi

    if rm -rf "$item"; then
        log INFO "Deleted: $item"
        ((files_deleted++))
        return 0
    else
        log ERROR "Failed to delete: $item"
        return 1
    fi
}

# Compare files
files_different() {
    local src="$1"
    local dst="$2"

    # Check if destination exists
    [ ! -f "$dst" ] && return 0

    # Compare modification times
    local src_mtime=$(stat -c %Y "$src" 2>/dev/null || stat -f %m "$src" 2>/dev/null)
    local dst_mtime=$(stat -c %Y "$dst" 2>/dev/null || stat -f %m "$dst" 2>/dev/null)

    [ "$src_mtime" -gt "$dst_mtime" ] && return 0

    # Compare sizes
    local src_size=$(stat -c %s "$src" 2>/dev/null || stat -f %z "$src" 2>/dev/null)
    local dst_size=$(stat -c %s "$dst" 2>/dev/null || stat -f %z "$dst" 2>/dev/null)

    [ "$src_size" -ne "$dst_size" ] && return 0

    return 1  # Files are the same
}

# Mirror sync (make target identical to source)
sync_mirror() {
    log INFO "Starting mirror synchronization"
    log INFO "Source: $SOURCE_DIR"
    log INFO "Target: $TARGET_DIR"

    # First pass: copy/update files from source to target
    find "$SOURCE_DIR" -type f | while IFS= read -r src_file; do
        # Skip excluded files
        if is_excluded "$src_file"; then
            log DEBUG "Excluded: $src_file"
            continue
        fi

        # Calculate relative path
        local rel_path="${src_file#$SOURCE_DIR/}"
        local dst_file="$TARGET_DIR/$rel_path"

        if files_different "$src_file" "$dst_file"; then
            if [ -f "$dst_file" ]; then
                copy_file "$src_file" "$dst_file" "update"
            else
                copy_file "$src_file" "$dst_file" "copy"
            fi
        fi
    done

    # Second pass: remove files from target that don't exist in source
    if [ -d "$TARGET_DIR" ]; then
        find "$TARGET_DIR" -type f | while IFS= read -r dst_file; do
            local rel_path="${dst_file#$TARGET_DIR/}"
            local src_file="$SOURCE_DIR/$rel_path"

            if [ ! -f "$src_file" ]; then
                delete_item "$dst_file"
            fi
        done

        # Remove empty directories
        find "$TARGET_DIR" -type d -empty -delete 2>/dev/null || true
    fi
}

# One-way sync (only copy new/updated files)
sync_oneway() {
    log INFO "Starting one-way synchronization"
    log INFO "Source: $SOURCE_DIR"
    log INFO "Target: $TARGET_DIR"

    find "$SOURCE_DIR" -type f | while IFS= read -r src_file; do
        # Skip excluded files
        if is_excluded "$src_file"; then
            log DEBUG "Excluded: $src_file"
            continue
        fi

        # Calculate relative path
        local rel_path="${src_file#$SOURCE_DIR/}"
        local dst_file="$TARGET_DIR/$rel_path"

        if files_different "$src_file" "$dst_file"; then
            if [ -f "$dst_file" ]; then
                copy_file "$src_file" "$dst_file" "update"
            else
                copy_file "$src_file" "$dst_file" "copy"
            fi
        fi
    done
}

# Bidirectional sync (sync both ways based on modification time)
sync_bidirectional() {
    log INFO "Starting bidirectional synchronization"
    log INFO "Directory A: $SOURCE_DIR"
    log INFO "Directory B: $TARGET_DIR"

    # Create list of all files in both directories
    local all_files_file="/tmp/sync_all_files.$$"

    {
        find "$SOURCE_DIR" -type f | sed "s|^$SOURCE_DIR/||"
        find "$TARGET_DIR" -type f | sed "s|^$TARGET_DIR/||"
    } | sort -u > "$all_files_file"

    while IFS= read -r rel_path; do
        # Skip excluded files
        if is_excluded "$rel_path"; then
            log DEBUG "Excluded: $rel_path"
            continue
        fi

        local file_a="$SOURCE_DIR/$rel_path"
        local file_b="$TARGET_DIR/$rel_path"

        if [ -f "$file_a" ] && [ -f "$file_b" ]; then
            # Both files exist, sync newer to older
            local mtime_a=$(stat -c %Y "$file_a" 2>/dev/null || stat -f %m "$file_a" 2>/dev/null)
            local mtime_b=$(stat -c %Y "$file_b" 2>/dev/null || stat -f %m "$file_b" 2>/dev/null)

            if [ "$mtime_a" -gt "$mtime_b" ]; then
                copy_file "$file_a" "$file_b" "update"
            elif [ "$mtime_b" -gt "$mtime_a" ]; then
                copy_file "$file_b" "$file_a" "update"
            fi
        elif [ -f "$file_a" ]; then
            # File only exists in A, copy to B
            copy_file "$file_a" "$file_b" "copy"
        elif [ -f "$file_b" ]; then
            # File only exists in B, copy to A
            copy_file "$file_b" "$file_a" "copy"
        fi
    done < "$all_files_file"

    rm -f "$all_files_file"
}

# Generate sync report
generate_report() {
    local report_file="sync_report_$(date +%Y%m%d_%H%M%S).txt"

    {
        echo "Directory Synchronization Report"
        echo "==============================="
        echo "Generated: $(date)"
        echo "Source: $SOURCE_DIR"
        echo "Target: $TARGET_DIR"
        echo "Sync Mode: $SYNC_MODE"
        echo "Dry Run: $DRY_RUN"
        echo

        echo "Statistics:"
        echo "==========="
        echo "Files copied: $files_copied"
        echo "Files updated: $files_updated"
        echo "Files deleted: $files_deleted"
        echo "Directories created: $dirs_created"
        echo "Bytes transferred: $(numfmt --to=iec-i --suffix=B $bytes_transferred)"
        echo

        echo "Log file: $LOG_FILE"

        if [ -f "$EXCLUDE_FILE" ]; then
            echo
            echo "Exclusion patterns:"
            echo "=================="
            cat "$EXCLUDE_FILE"
        fi

    } > "$report_file"

    log INFO "Report generated: $report_file"
}

# Validate directories
validate_directories() {
    if [ ! -d "$SOURCE_DIR" ]; then
        log ERROR "Source directory does not exist: $SOURCE_DIR"
        exit 1
    fi

    if [ "$SYNC_MODE" != "bidirectional" ] && [ ! -d "$TARGET_DIR" ]; then
        log INFO "Creating target directory: $TARGET_DIR"
        if ! mkdir -p "$TARGET_DIR"; then
            log ERROR "Failed to create target directory: $TARGET_DIR"
            exit 1
        fi
    fi

    # Check for circular sync (target inside source or vice versa)
    local source_real=$(realpath "$SOURCE_DIR")
    local target_real=$(realpath "$TARGET_DIR" 2>/dev/null || echo "$TARGET_DIR")

    if [[ "$target_real" == "$source_real"* ]] || [[ "$source_real" == "$target_real"* ]]; then
        log ERROR "Circular sync detected: target cannot be inside source or vice versa"
        exit 1
    fi
}

# Show progress
show_progress() {
    if [ "$DRY_RUN" != "true" ]; then
        while true; do
            echo -ne "\rFiles: $files_copied copied, $files_updated updated, $files_deleted deleted"
            sleep 1
        done &
        local progress_pid=$!

        # Kill progress display when script exits
        trap "kill $progress_pid 2>/dev/null" EXIT
    fi
}

# Main synchronization function
main() {
    log INFO "Starting directory synchronization"

    validate_directories

    if [ "$DRY_RUN" = "true" ]; then
        log INFO "DRY RUN MODE - No files will be modified"
    fi

    show_progress

    case "$SYNC_MODE" in
        mirror)
            sync_mirror
            ;;
        oneway)
            sync_oneway
            ;;
        bidirectional)
            sync_bidirectional
            ;;
        *)
            log ERROR "Unknown sync mode: $SYNC_MODE"
            exit 1
            ;;
    esac

    log INFO "Synchronization completed"

    echo
    echo "Synchronization Summary:"
    echo "======================="
    echo "Files copied: $files_copied"
    echo "Files updated: $files_updated"
    echo "Files deleted: $files_deleted"
    echo "Directories created: $dirs_created"
    echo "Bytes transferred: $(numfmt --to=iec-i --suffix=B $bytes_transferred)"

    generate_report
}

# Show usage
show_usage() {
    cat << EOF
Directory Synchronizer - Synchronize directories with various options

Usage: $0 <source_dir> <target_dir> [options]

Options:
  -m, --mode MODE         Sync mode: mirror, oneway, bidirectional
  -n, --dry-run          Show what would be done without making changes
  -e, --exclude FILE     File containing exclusion patterns
  -l, --log FILE         Log file path
  --help                 Show this help

Environment Variables:
  SYNC_MODE              Sync mode
  DRY_RUN               Dry run mode (true/false)
  EXCLUDE_FILE          Exclusion patterns file
  LOG_FILE              Log file path

Sync Modes:
  mirror                Make target identical to source (default)
  oneway                Copy new/updated files from source to target
  bidirectional         Sync both ways based on modification time

Examples:
  $0 /home/user/docs /backup/docs
  $0 /home/user/docs /backup/docs --mode oneway --dry-run
  SYNC_MODE=bidirectional $0 /home/user/docs /shared/docs
EOF
}

# Parse command line arguments
while [[ $# -gt 0 ]]; do
    case $1 in
        -m|--mode)
            SYNC_MODE="$2"
            shift 2
            ;;
        -n|--dry-run)
            DRY_RUN="true"
            shift
            ;;
        -e|--exclude)
            EXCLUDE_FILE="$2"
            shift 2
            ;;
        -l|--log)
            LOG_FILE="$2"
            shift 2
            ;;
        --help)
            show_usage
            exit 0
            ;;
        -*)
            echo "Unknown option: $1"
            show_usage
            exit 1
            ;;
        *)
            if [ -z "$SOURCE_DIR" ]; then
                SOURCE_DIR="$1"
            elif [ -z "$TARGET_DIR" ]; then
                TARGET_DIR="$1"
            fi
            shift
            ;;
    esac
done

# Validate required arguments
if [ -z "$SOURCE_DIR" ] || [ -z "$TARGET_DIR" ]; then
    echo "Error: Both source and target directories are required"
    show_usage
    exit 1
fi

main

System Maintenance Utilities

Log Rotator

#!/bin/bash
# log_rotator.sh - Advanced log rotation utility

CONFIG_FILE="${CONFIG_FILE:-/etc/logrotate_custom.conf}"
LOG_DIR="${LOG_DIR:-/var/log}"
MAX_AGE_DAYS="${MAX_AGE_DAYS:-30}"
COMPRESS="${COMPRESS:-true}"
COMPRESSION_DELAY="${COMPRESSION_DELAY:-1}"

# Default configuration
declare -A default_config=(
    [rotate]="7"
    [size]="100M"
    [compress]="true"
    [delaycompress]="true"
    [missingok]="true"
    [notifempty]="true"
    [create]="644 root root"
)

# Logging function
log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" | tee -a "/var/log/log_rotator.log"
}

# Parse size string to bytes
parse_size() {
    local size_str="$1"
    local size_num=$(echo "$size_str" | sed 's/[^0-9]*//g')
    local size_unit=$(echo "$size_str" | sed 's/[0-9]*//g' | tr '[:lower:]' '[:upper:]')

    case "$size_unit" in
        K|KB) echo $((size_num * 1024)) ;;
        M|MB) echo $((size_num * 1024 * 1024)) ;;
        G|GB) echo $((size_num * 1024 * 1024 * 1024)) ;;
        *) echo "$size_num" ;;
    esac
}

# Check if file needs rotation
needs_rotation() {
    local log_file="$1"
    local max_size="$2"
    local rotate_count="$3"

    # Check if file exists and is not empty
    if [ ! -f "$log_file" ] || [ ! -s "$log_file" ]; then
        return 1
    fi

    # Check size
    local file_size=$(stat -c%s "$log_file" 2>/dev/null || stat -f%z "$log_file" 2>/dev/null)
    local max_bytes=$(parse_size "$max_size")

    if [ "$file_size" -gt "$max_bytes" ]; then
        return 0
    fi

    # Check if we have reached max rotations
    if [ -f "${log_file}.${rotate_count}" ]; then
        return 0
    fi

    return 1
}

# Rotate a single log file
rotate_log() {
    local log_file="$1"
    local config_name="$2"

    # Load configuration for this log
    local rotate_count="${log_configs[$config_name.rotate]:-${default_config[rotate]}}"
    local max_size="${log_configs[$config_name.size]:-${default_config[size]}}"
    local compress="${log_configs[$config_name.compress]:-${default_config[compress]}}"
    local delaycompress="${log_configs[$config_name.delaycompress]:-${default_config[delaycompress]}}"
    local missingok="${log_configs[$config_name.missingok]:-${default_config[missingok]}}"
    local notifempty="${log_configs[$config_name.notifempty]:-${default_config[notifempty]}}"
    local create_mode="${log_configs[$config_name.create]:-${default_config[create]}}"

    # Check if rotation is needed
    if ! needs_rotation "$log_file" "$max_size" "$rotate_count"; then
        log "No rotation needed for: $log_file"
        return 0
    fi

    log "Rotating log file: $log_file"

    # Remove oldest log if it exists
    if [ -f "${log_file}.${rotate_count}" ]; then
        rm "${log_file}.${rotate_count}"
        log "Removed oldest log: ${log_file}.${rotate_count}"
    fi

    # Rotate existing logs
    for ((i=rotate_count-1; i>=1; i--)); do
        local current_log="${log_file}.${i}"
        local next_log="${log_file}.$((i+1))"

        if [ -f "$current_log" ]; then
            mv "$current_log" "$next_log"
            log "Moved: $current_log -> $next_log"
        fi
    done

    # Move current log to .1
    if [ -f "$log_file" ]; then
        mv "$log_file" "${log_file}.1"
        log "Moved: $log_file -> ${log_file}.1"

        # Create new log file with proper permissions
        if [ -n "$create_mode" ]; then
            local mode=$(echo "$create_mode" | awk '{print $1}')
            local owner=$(echo "$create_mode" | awk '{print $2}')
            local group=$(echo "$create_mode" | awk '{print $3}')

            touch "$log_file"
            chmod "$mode" "$log_file" 2>/dev/null || true
            chown "$owner:$group" "$log_file" 2>/dev/null || true

            log "Created new log file: $log_file ($mode $owner:$group)"
        fi
    fi

    # Compress rotated logs
    if [ "$compress" = "true" ]; then
        local start_index=1
        if [ "$delaycompress" = "true" ]; then
            start_index=2
        fi

        for ((i=start_index; i<=rotate_count; i++)); do
            local log_to_compress="${log_file}.${i}"
            if [ -f "$log_to_compress" ] && [[ ! "$log_to_compress" =~ \.gz$ ]]; then
                if gzip "$log_to_compress"; then
                    log "Compressed: $log_to_compress"
                else
                    log "Failed to compress: $log_to_compress"
                fi
            fi
        done
    fi

    # Execute post-rotation script if configured
    local postrotate_script="${log_configs[$config_name.postrotate]:-}"
    if [ -n "$postrotate_script" ]; then
        log "Executing post-rotation script: $postrotate_script"
        if eval "$postrotate_script"; then
            log "Post-rotation script completed successfully"
        else
            log "Post-rotation script failed"
        fi
    fi
}

# Load configuration file
load_config() {
    declare -gA log_configs

    if [ ! -f "$CONFIG_FILE" ]; then
        log "Configuration file not found: $CONFIG_FILE"
        return 1
    fi

    local current_section=""

    while IFS= read -r line; do
        # Skip comments and empty lines
        [[ $line =~ ^[[:space:]]*# ]] && continue
        [[ -z $line ]] && continue

        # Check for section header
        if [[ $line =~ ^\[([^\]]+)\] ]]; then
            current_section="${BASH_REMATCH[1]}"
            continue
        fi

        # Parse key=value pairs
        if [[ $line =~ ^[[:space:]]*([^=]+)=(.*)$ ]]; then
            local key="${BASH_REMATCH[1]// /}"
            local value="${BASH_REMATCH[2]}"

            if [ -n "$current_section" ]; then
                log_configs["$current_section.$key"]="$value"
            else
                log_configs["$key"]="$value"
            fi
        fi
    done < "$CONFIG_FILE"

    log "Configuration loaded from: $CONFIG_FILE"
}

# Generate default configuration
generate_config() {
    cat > "$CONFIG_FILE" << EOF
# Log Rotation Configuration
# Generated on $(date)

# Global settings
compress=true
delaycompress=true
missingok=true
notifempty=true

# Apache logs
[apache]
files=/var/log/apache2/*.log
rotate=52
size=100M
create=644 www-data www-data
postrotate=systemctl reload apache2

# Nginx logs
[nginx]
files=/var/log/nginx/*.log
rotate=52
size=100M
create=644 www-data www-data
postrotate=systemctl reload nginx

# System logs
[syslog]
files=/var/log/syslog
rotate=7
size=50M
create=644 syslog adm

# Application logs
[application]
files=/var/log/myapp/*.log
rotate=30
size=10M
create=644 myapp myapp
postrotate=killall -USR1 myapp
EOF

    log "Configuration template created: $CONFIG_FILE"
}

# Clean old compressed logs
cleanup_old_logs() {
    log "Cleaning up logs older than $MAX_AGE_DAYS days"

    local deleted_count=0
    local freed_space=0

    find "$LOG_DIR" -name "*.gz" -mtime +$MAX_AGE_DAYS -type f | while read -r old_log; do
        local file_size=$(stat -c%s "$old_log" 2>/dev/null || stat -f%z "$old_log" 2>/dev/null)

        if rm "$old_log"; then
            log "Deleted old log: $old_log ($(numfmt --to=iec-i --suffix=B $file_size))"
            ((deleted_count++))
            freed_space=$((freed_space + file_size))
        fi
    done

    log "Cleanup completed: $deleted_count files deleted, $(numfmt --to=iec-i --suffix=B $freed_space) freed"
}

# Show log statistics
show_statistics() {
    echo "Log File Statistics"
    echo "=================="
    echo "Generated: $(date)"
    echo

    # Count log files by type
    echo "Log Files by Extension:"
    echo "======================"
    find "$LOG_DIR" -type f -name "*.log*" | sed 's/.*\.//' | sort | uniq -c | sort -nr

    echo
    echo "Largest Log Files:"
    echo "=================="
    find "$LOG_DIR" -type f -name "*.log*" -exec ls -lh {} \; | sort -k5 -hr | head -10

    echo
    echo "Disk Usage by Directory:"
    echo "======================="
    du -h "$LOG_DIR"/* 2>/dev/null | sort -hr | head -10

    echo
    echo "Compressed vs Uncompressed:"
    echo "=========================="
    local compressed_count=$(find "$LOG_DIR" -name "*.gz" -type f | wc -l)
    local uncompressed_count=$(find "$LOG_DIR" -name "*.log" -type f | wc -l)
    local compressed_size=$(find "$LOG_DIR" -name "*.gz" -type f -exec stat -c%s {} \; 2>/dev/null | awk '{sum+=$1} END {print sum+0}')
    local uncompressed_size=$(find "$LOG_DIR" -name "*.log" -type f -exec stat -c%s {} \; 2>/dev/null | awk '{sum+=$1} END {print sum+0}')

    echo "Compressed files: $compressed_count ($(numfmt --to=iec-i --suffix=B $compressed_size))"
    echo "Uncompressed files: $uncompressed_count ($(numfmt --to=iec-i --suffix=B $uncompressed_size))"
}

# Main rotation process
main() {
    log "Starting log rotation process"

    if ! load_config; then
        log "Failed to load configuration, using defaults"
    fi

    # Get list of configured log sections
    local sections=()
    for key in "${!log_configs[@]}"; do
        if [[ $key =~ ^([^.]+)\.files$ ]]; then
            sections+=("${BASH_REMATCH[1]}")
        fi
    done

    # Remove duplicates
    IFS=$'\n' sections=($(sort -u <<<"${sections[*]}"))
    unset IFS

    if [ ${#sections[@]} -eq 0 ]; then
        log "No log sections configured, rotating all .log files in $LOG_DIR"

        # Rotate all .log files with default settings
        find "$LOG_DIR" -name "*.log" -type f | while read -r log_file; do
            rotate_log "$log_file" "default"
        done
    else
        # Rotate configured logs
        for section in "${sections[@]}"; do
            local files_pattern="${log_configs[$section.files]}"

            if [ -n "$files_pattern" ]; then
                log "Processing section: $section ($files_pattern)"

                # Expand file pattern
                for log_file in $files_pattern; do
                    if [ -f "$log_file" ]; then
                        rotate_log "$log_file" "$section"
                    fi
                done
            fi
        done
    fi

    # Cleanup old logs
    cleanup_old_logs

    log "Log rotation process completed"
}

# Show usage
show_usage() {
    cat << EOF
Log Rotator - Advanced log rotation utility

Usage: $0 [command] [options]

Commands:
  rotate                 Perform log rotation (default)
  config                 Generate configuration template
  stats                  Show log file statistics
  cleanup                Clean up old compressed logs only

Options:
  -c, --config FILE      Configuration file path
  -d, --log-dir DIR      Log directory path
  -a, --max-age DAYS     Maximum age for log cleanup
  --help                 Show this help

Environment Variables:
  CONFIG_FILE            Configuration file path
  LOG_DIR               Log directory path
  MAX_AGE_DAYS          Maximum age for cleanup

Examples:
  $0                     # Rotate logs using default config
  $0 config              # Generate configuration template
  $0 stats               # Show log statistics
  $0 cleanup --max-age 7 # Clean logs older than 7 days
EOF
}

# Parse command line arguments
COMMAND="rotate"

while [[ $# -gt 0 ]]; do
    case $1 in
        rotate|config|stats|cleanup)
            COMMAND="$1"
            shift
            ;;
        -c|--config)
            CONFIG_FILE="$2"
            shift 2
            ;;
        -d|--log-dir)
            LOG_DIR="$2"
            shift 2
            ;;
        -a|--max-age)
            MAX_AGE_DAYS="$2"
            shift 2
            ;;
        --help)
            show_usage
            exit 0
            ;;
        -*)
            echo "Unknown option: $1"
            show_usage
            exit 1
            ;;
        *)
            shift
            ;;
    esac
done

# Execute command
case "$COMMAND" in
    rotate)
        main
        ;;
    config)
        generate_config
        ;;
    stats)
        show_statistics
        ;;
    cleanup)
        cleanup_old_logs
        ;;
    *)
        echo "Unknown command: $COMMAND"
        show_usage
        exit 1
        ;;
esac

These utility scripts provide comprehensive solutions for common file management and system maintenance tasks. Each script includes extensive configuration options, error handling, and detailed logging to make them suitable for production use.