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
mainDirectory 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
mainSystem 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
;;
esacThese 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.