Skip to content

Instantly share code, notes, and snippets.

@1999AZZAR
Last active December 29, 2024 15:44
Show Gist options
  • Save 1999AZZAR/35caa1acdcfedf2b8e0435aa48232aee to your computer and use it in GitHub Desktop.
Save 1999AZZAR/35caa1acdcfedf2b8e0435aa48232aee to your computer and use it in GitHub Desktop.
Dotfiles Management Script (DMS) A robust and safe bash script for managing your dotfiles through symlinks, with support for backup, restoration, and maintenance operations.

Dotfiles Management Script

A robust and safe bash script for managing your dotfiles through symlinks, with support for backup, restoration, and maintenance operations.

Table of Contents

Overview

This script provides a comprehensive solution for managing dotfiles (configuration files) in Unix-like systems. It helps you maintain a centralized repository of your configuration files while creating symbolic links to their original locations, making it easy to version control and synchronize your settings across different machines.

Features

  • Automated backup and symlink creation for .config files
  • Safe restoration of original configuration files
  • Home directory dotfiles management
  • Automatic backup creation before any operation
  • Intelligent exclusion of cache and temporary files
  • Detection and cleanup of broken symlinks
  • Error handling and operation validation
  • Colored output for better readability

Prerequisites

  • Bash shell (version 4.0 or higher)
  • Standard Unix utilities:
    • ln (for creating symlinks)
    • find (for file operations)
    • cp (for copying files)
    • rm (for removing files)
    • mkdir (for creating directories)
    • date (for timestamp generation)

Installation

  1. Clone or download the script to your preferred location.
  2. Make the script executable:
chmod +x management.sh

Usage

Run the script:

./management.sh

The script will present a menu with the following options:

  1. Backup and symlink .config files
  2. Restore .config files
  3. Create symlinks for home directory dotfiles
  4. Remove broken/unknown symlinks
  5. Exit

Directory Structure

$HOME/
├── .config/                # Original configuration directory
├── dotfiles/               # Your dotfiles repository
│   └── .config/            # Managed configuration files
└── .dotfiles_backups/      # Backup directory
    ├── config_TIMESTAMP/
    ├── restore_TIMESTAMP/
    ├── symlink_TIMESTAMP/
    └── symlink_cleanup_TIMESTAMP/

Operations

1. Backup and Symlink

  • Moves configuration files from .config to the dotfiles repository
  • Creates symlinks in the original location
  • Automatically backs up existing files
  • Skips cache and temporary files
# Example: Managing neovim configuration
Original: ~/.config/nvim/init.vim
After: ~/dotfiles/.config/nvim/init.vim -> ~/.config/nvim/init.vim (symlink)

2. Restore Configuration

  • Removes symlinks and restores original files
  • Creates backup before restoration
  • Validates symlink targets before removal
  • Handles conflicts safely

3. Home Directory Dotfiles

  • Creates symlinks for files in the root of dotfiles directory
  • Useful for files like .bashrc, .vimrc, etc.
  • Maintains backup of existing files

4. Symlink Cleanup

  • Identifies and removes broken symlinks
  • Removes unknown symlinks (not pointing to dotfiles)
  • Creates backup of removed symlinks
  • Provides detailed operation feedback

Backup System

The script maintains a comprehensive backup system:

  • Location: $HOME/.dotfiles_backups/
  • Naming: <operation>_YYYYMMDD_HHMMSS/
  • Types:
    • config_*: Backups of existing dotfiles
    • restore_*: Backups before restoration
    • symlink_*: Backups of existing files before symlinking
    • symlink_cleanup_*: Backups of removed symlinks

Safety Features

  1. Error Handling:

    • Strict error checking with set -euo pipefail
    • Trap handler for script errors
    • Validation of required commands
  2. Operation Safety:

    • Automatic backups before operations
    • Validation of symlink targets
    • Safe handling of existing files
    • Prevention of data loss

Exclusion System

The script automatically excludes:

  1. Common cache and temporary files:

    • Cache directories
    • Log files
    • Temporary files
    • History files
    • State files
    • Socket files
  2. Pattern-based exclusion:

    EXCLUDE_PATTERNS=(
        'cache' 'logs?' 'tmp' 'temp' 'history' 'state'
        'socket' 'log' 'crash' 'dump' 'index' '.git'
        '.local/share/recently-used.xbel'
        '.cache' '.thumbnails' '.npm' '.yarn'
    )
  3. Content-based exclusion:

    • Directories with >50% cache/temporary files
    • Known cache file patterns

Troubleshooting

Common Issues

  1. Permission Denied
# Solution
chmod +x management.sh
  1. Broken Symlinks
# Check symlinks
ls -la ~/.config/
# Run symlink cleanup
./management.sh  # Select option 4
  1. Backup Recovery
# Backups are located in
ls -la ~/.dotfiles_backups/
# Copy desired files from backup
cp ~/.dotfiles_backups/config_<timestamp>/<file> ~/.config/
#!/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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment