|
#!/bin/bash |
|
# A unified script for managing dotfiles: setup, backup, and restoration |
|
|
|
set -euo pipefail # Enable strict error handling |
|
|
|
# Define common paths |
|
readonly DOTFILES_DIR="$HOME/dotfiles" |
|
readonly CONFIG_SRC="$HOME/.config" |
|
readonly DOTFILES_CONFIG="$DOTFILES_DIR/.config" |
|
readonly BACKUP_DIR="$HOME/.dotfiles_backups" |
|
|
|
# Function to print colored messages |
|
print_status() { |
|
local color=$1 |
|
local message=$2 |
|
case $color in |
|
"green") echo -e "\033[0;32m$message\033[0m" ;; |
|
"yellow") echo -e "\033[0;33m$message\033[0m" ;; |
|
"red") echo -e "\033[0;31m$message\033[0m" ;; |
|
*) echo "$message" ;; |
|
esac |
|
} |
|
|
|
# Function to handle errors |
|
error_handler() { |
|
local line_no=$1 |
|
local command=$2 |
|
print_status "red" "Error occurred in script at line $line_no" |
|
print_status "red" "Failed command: $command" |
|
exit 1 |
|
} |
|
trap 'error_handler ${LINENO} "${BASH_COMMAND}"' ERR |
|
|
|
# Function to create backup directory with timestamp |
|
create_backup_dir() { |
|
local prefix=$1 |
|
local timestamp=$(date +%Y%m%d_%H%M%S) |
|
local backup_path="${BACKUP_DIR}/${prefix}_${timestamp}" |
|
mkdir -p "$backup_path" |
|
echo "$backup_path" |
|
} |
|
|
|
# Enhanced exclusion patterns |
|
declare -a EXCLUDE_PATTERNS=( |
|
'cache' 'logs?' 'tmp' 'temp' 'history' 'state' |
|
'socket' 'log' 'crash' 'dump' 'index' '\.git' |
|
'\.local/share/recently-used\.xbel' |
|
'\.cache' '\.thumbnails' '\.npm' '\.yarn' |
|
) |
|
|
|
# Special items to never symlink |
|
declare -a SPECIAL_EXCLUSIONS=( |
|
'wallpapers' |
|
'readme.md' |
|
'README.md' |
|
'Readme.md' |
|
'management.sh' |
|
) |
|
|
|
# Function to check if an item should be excluded from processing |
|
should_exclude() { |
|
local item_name="$1" |
|
local item_path="$2" |
|
|
|
# Check against special exclusions (case-insensitive) |
|
local special_item |
|
for special_item in "${SPECIAL_EXCLUSIONS[@]}"; do |
|
if [[ "${item_name,,}" == "${special_item,,}" ]]; then |
|
print_status "yellow" "Skipping special excluded item: $item_name" |
|
return 0 |
|
fi |
|
done |
|
|
|
# Check against regular exclusion patterns |
|
local pattern |
|
for pattern in "${EXCLUDE_PATTERNS[@]}"; do |
|
if [[ "${item_name,,}" =~ ${pattern} ]]; then |
|
return 0 |
|
fi |
|
done |
|
|
|
# Check directory content for cache/temporary files |
|
if [[ -d "$item_path" ]]; then |
|
local cache_count=0 |
|
local total_count=0 |
|
|
|
# Safely count files |
|
if total_count=$(find "$item_path" -type f 2>/dev/null | wc -l); then |
|
local pattern_regex |
|
pattern_regex="$(printf "%s|" "${EXCLUDE_PATTERNS[@]}")" |
|
pattern_regex="${pattern_regex%|}" |
|
|
|
if cache_count=$(find "$item_path" -type f -regextype posix-extended \ |
|
-iregex ".*/(${pattern_regex}).*" 2>/dev/null | wc -l); then |
|
if [[ $total_count -gt 0 && $((cache_count * 100 / total_count)) -gt 50 ]]; then |
|
return 0 |
|
fi |
|
fi |
|
fi |
|
fi |
|
|
|
return 1 |
|
} |
|
|
|
# Function to safely create symlinks |
|
create_symlinks() { |
|
local src_dir="$1" |
|
local dest_dir="$2" |
|
local backup_dir |
|
|
|
backup_dir=$(create_backup_dir "symlink") || { |
|
print_status "red" "Failed to create backup directory" |
|
return 1 |
|
} |
|
|
|
local item |
|
for item in "$src_dir"/*; do |
|
[[ -e "$item" ]] || continue # Skip if no matches found |
|
|
|
local item_name |
|
item_name=$(basename "$item") |
|
|
|
if should_exclude "$item_name" "$item"; then |
|
print_status "yellow" "Skipping excluded item: $item_name" |
|
continue |
|
fi |
|
|
|
local dest_path="$dest_dir/$item_name" |
|
|
|
# Check if symlink already exists and points to the correct target |
|
if [[ -L "$dest_path" ]]; then |
|
local current_target |
|
current_target=$(readlink "$dest_path") |
|
if [[ "$current_target" == "$item" ]]; then |
|
print_status "yellow" "Symlink already exists and is correct: $item_name" |
|
continue |
|
fi |
|
fi |
|
|
|
# Backup existing file/directory if it's not a symlink |
|
if [[ -e "$dest_path" && ! -L "$dest_path" ]]; then |
|
print_status "yellow" "Backing up existing file: $item_name" |
|
mv "$dest_path" "$backup_dir/" || { |
|
print_status "red" "Failed to backup: $item_name" |
|
continue |
|
} |
|
# Remove existing symlink if it points to wrong target |
|
elif [[ -L "$dest_path" ]]; then |
|
print_status "yellow" "Removing incorrect symlink: $item_name" |
|
rm "$dest_path" || { |
|
print_status "red" "Failed to remove existing symlink: $item_name" |
|
continue |
|
} |
|
fi |
|
|
|
ln -sf "$item" "$dest_path" && \ |
|
print_status "green" "Created symlink for $item_name to $dest_dir" || \ |
|
print_status "red" "Failed to create symlink for $item_name" |
|
done |
|
} |
|
|
|
# Enhanced backup and symlink function |
|
backup_and_symlink() { |
|
mkdir -p "$DOTFILES_CONFIG" || { |
|
print_status "red" "Failed to create dotfiles config directory" |
|
return 1 |
|
} |
|
|
|
local backup_dir |
|
backup_dir=$(create_backup_dir "config") || { |
|
print_status "red" "Failed to create backup directory" |
|
return 1 |
|
} |
|
|
|
# Backup existing dotfiles |
|
if [[ -d "$DOTFILES_CONFIG" ]]; then |
|
print_status "yellow" "Creating backup of existing dotfiles" |
|
cp -a "$DOTFILES_CONFIG/." "$backup_dir/" || { |
|
print_status "red" "Failed to backup existing dotfiles" |
|
return 1 |
|
} |
|
fi |
|
|
|
# Process .config items |
|
local item |
|
find "$CONFIG_SRC" -mindepth 1 -maxdepth 1 -print0 | while IFS= read -r -d '' item; do |
|
local item_name |
|
item_name=$(basename "$item") |
|
|
|
if should_exclude "$item_name" "$item"; then |
|
print_status "yellow" "Skipping excluded item: $item_name" |
|
continue |
|
fi |
|
|
|
local dotfile_path="$DOTFILES_CONFIG/$item_name" |
|
if [[ -e "$item" && ! -e "$dotfile_path" ]]; then |
|
print_status "yellow" "Moving $item_name to dotfiles" |
|
cp -a "$item" "$dotfile_path" || { |
|
print_status "red" "Failed to copy $item_name to dotfiles" |
|
continue |
|
} |
|
rm -rf "$item" || print_status "red" "Failed to remove original $item_name" |
|
fi |
|
|
|
ln -sf "$dotfile_path" "$item" && \ |
|
print_status "green" "Created/updated symlink for $item_name" || \ |
|
print_status "red" "Failed to create symlink for $item_name" |
|
done |
|
} |
|
|
|
# Enhanced restore function with error handling |
|
restore_config() { |
|
local backup_dir |
|
backup_dir=$(create_backup_dir "restore") || { |
|
print_status "red" "Failed to create backup directory" |
|
return 1 |
|
} |
|
|
|
print_status "yellow" "Creating backup of current .config" |
|
cp -a "$CONFIG_SRC/." "$backup_dir/" || { |
|
print_status "red" "Failed to backup current .config" |
|
return 1 |
|
} |
|
|
|
if [[ ! -d "$DOTFILES_CONFIG" ]]; then |
|
print_status "red" "Dotfiles directory not found: $DOTFILES_CONFIG" |
|
return 1 |
|
fi |
|
|
|
local item |
|
find "$DOTFILES_CONFIG" -mindepth 1 -maxdepth 1 -print0 | while IFS= read -r -d '' item; do |
|
local item_name |
|
item_name=$(basename "$item") |
|
local config_path="$CONFIG_SRC/$item_name" |
|
|
|
if [[ -L "$config_path" ]]; then |
|
local link_target |
|
link_target=$(readlink "$config_path") |
|
if [[ "$link_target" == "$DOTFILES_CONFIG/$item_name" ]]; then |
|
rm "$config_path" && \ |
|
cp -a "$item" "$config_path" && \ |
|
print_status "green" "Restored $item_name to .config" || \ |
|
print_status "red" "Failed to restore $item_name" |
|
else |
|
print_status "yellow" "Skipping $item_name - symlink points elsewhere" |
|
fi |
|
elif [[ ! -e "$config_path" ]]; then |
|
cp -a "$item" "$config_path" && \ |
|
print_status "green" "Restored $item_name to .config" || \ |
|
print_status "red" "Failed to restore $item_name" |
|
else |
|
print_status "yellow" "Skipping $item_name - target exists and is not a symlink" |
|
fi |
|
done |
|
|
|
# Clean up empty directories |
|
if [[ -d "$DOTFILES_CONFIG" && -z "$(ls -A "$DOTFILES_CONFIG")" ]]; then |
|
print_status "yellow" "Cleaning up empty dotfiles directory" |
|
rm -r "$DOTFILES_CONFIG" || print_status "red" "Failed to remove empty dotfiles directory" |
|
[[ -d "$DOTFILES_DIR" && -z "$(ls -A "$DOTFILES_DIR")" ]] && \ |
|
rm -r "$DOTFILES_DIR" || print_status "red" "Failed to remove empty dotfiles parent directory" |
|
fi |
|
} |
|
|
|
# Enhanced symlink cleanup function with better error handling |
|
remove_symlinks() { |
|
local backup_dir |
|
backup_dir=$(create_backup_dir "symlink_cleanup") || { |
|
print_status "red" "Failed to create backup directory" |
|
return 1 |
|
} |
|
|
|
local symlink |
|
find "$CONFIG_SRC" -type l -print0 | while IFS= read -r -d '' symlink; do |
|
local target |
|
target=$(readlink "$symlink") || { |
|
print_status "red" "Failed to read symlink target: $symlink" |
|
continue |
|
} |
|
|
|
local rel_path="${symlink#$CONFIG_SRC/}" |
|
|
|
if [[ ! -e "$symlink" || "$target" != "$DOTFILES_CONFIG"* ]]; then |
|
local backup_path="$backup_dir/$rel_path" |
|
mkdir -p "$(dirname "$backup_path")" || { |
|
print_status "red" "Failed to create backup directory structure for: $rel_path" |
|
continue |
|
} |
|
|
|
cp -P "$symlink" "$backup_path" || { |
|
print_status "red" "Failed to backup symlink: $rel_path" |
|
continue |
|
} |
|
|
|
rm "$symlink" || { |
|
print_status "red" "Failed to remove symlink: $rel_path" |
|
continue |
|
} |
|
|
|
if [[ ! -e "$symlink" ]]; then |
|
print_status "green" "Removed broken symlink: $rel_path -> $target" |
|
else |
|
print_status "yellow" "Removed unknown symlink: $rel_path -> $target" |
|
fi |
|
fi |
|
done |
|
|
|
print_status "green" "Symlink cleanup completed. Backup created at: $backup_dir" |
|
} |
|
|
|
# Enhanced requirements check |
|
check_requirements() { |
|
local required_commands=("ln" "find" "cp" "rm" "mkdir" "date" "readlink") |
|
local missing_commands=() |
|
|
|
for cmd in "${required_commands[@]}"; do |
|
if ! command -v "$cmd" >/dev/null 2>&1; then |
|
missing_commands+=("$cmd") |
|
fi |
|
done |
|
|
|
if ((${#missing_commands[@]} > 0)); then |
|
print_status "red" "Required commands not found: ${missing_commands[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Interactive menu with improved input handling |
|
main_menu() { |
|
while true; do |
|
echo |
|
echo "Dotfiles Management Menu" |
|
echo "----------------------" |
|
echo "1) Backup and symlink .config files" |
|
echo "2) Restore .config files" |
|
echo "3) Create symlinks for home directory dotfiles" |
|
echo "4) Remove broken/unknown symlinks" |
|
echo "5) Exit" |
|
echo |
|
|
|
local choice |
|
read -rp "Enter your choice (1-5): " choice |
|
|
|
case $choice in |
|
1) backup_and_symlink || print_status "red" "Backup and symlink operation failed" ;; |
|
2) restore_config || print_status "red" "Restore operation failed" ;; |
|
3) |
|
if ! mkdir -p "$HOME/.config"; then |
|
print_status "red" "Failed to create .config directory" |
|
continue |
|
fi |
|
print_status "yellow" "Creating symlinks for home directory dotfiles..." |
|
if create_symlinks "$DOTFILES_DIR" "$HOME"; then |
|
print_status "green" "Home directory dotfiles symlinked successfully!" |
|
else |
|
print_status "red" "Failed to create home directory symlinks" |
|
fi |
|
;; |
|
4) remove_symlinks || print_status "red" "Symlink cleanup operation failed" ;; |
|
5) |
|
print_status "green" "Exiting script. Goodbye!" |
|
exit 0 |
|
;; |
|
*) |
|
print_status "red" "Invalid choice, please enter a number between 1 and 5." |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
# Script initialization |
|
check_requirements |
|
mkdir -p "$BACKUP_DIR" || { |
|
print_status "red" "Failed to create backup directory" |
|
exit 1 |
|
} |
|
main_menu |