415 lines
13 KiB
Bash
Executable File
415 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# Dotfiles Setup Script
|
|
# Description: Install dotfiles using GNU Stow with backup and selective installation
|
|
|
|
set -e
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
BACKUP_DIR="$HOME/.dotfiles-backup-$(date +%Y%m%d-%H%M%S)"
|
|
AVAILABLE_PACKAGES=(bash zsh git ssh starship)
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Functions
|
|
log_info() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
}
|
|
|
|
log_warning() {
|
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
Dotfiles Setup Script
|
|
|
|
USAGE:
|
|
$0 [OPTIONS]
|
|
|
|
OPTIONS:
|
|
-h, --help Show this help message
|
|
-d, --dry-run Show what would be done without making changes
|
|
-v, --verbose Enable verbose output
|
|
-q, --quiet Suppress non-essential output
|
|
--only=PACKAGES Install only specified packages (comma-separated)
|
|
Available: ${AVAILABLE_PACKAGES[*]}
|
|
--backup-dir=DIR Custom backup directory (default: ~/.dotfiles-backup-TIMESTAMP)
|
|
|
|
EXAMPLES:
|
|
$0 # Install all configurations
|
|
$0 --only=git,zsh # Install only git and zsh configurations
|
|
$0 --dry-run # Preview what would be installed
|
|
$0 --verbose # Install with detailed output
|
|
|
|
AVAILABLE PACKAGES:
|
|
EOF
|
|
for package in "${AVAILABLE_PACKAGES[@]}"; do
|
|
echo " - $package"
|
|
done
|
|
}
|
|
|
|
check_dependencies() {
|
|
log_info "Checking dependencies..."
|
|
|
|
if ! command -v stow &> /dev/null; then
|
|
log_error "GNU Stow is not installed"
|
|
log_info "Installing GNU Stow..."
|
|
if command -v apt-get &> /dev/null; then
|
|
sudo apt-get update && sudo apt-get install -y stow
|
|
else
|
|
log_error "Cannot install stow automatically. Please install it manually."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
log_success "All dependencies satisfied"
|
|
}
|
|
|
|
install_toolchain() {
|
|
local bin_dir="$HOME/.local/bin"
|
|
mkdir -p "$bin_dir"
|
|
|
|
# Install zinit (zsh plugin manager) if missing
|
|
if [[ ! -f "$HOME/.local/share/zinit/zinit.git/zinit.zsh" ]]; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install zinit"
|
|
else
|
|
log_info "Installing zinit..."
|
|
command mkdir -p "$HOME/.local/share/zinit" && command chmod g-rwX "$HOME/.local/share/zinit"
|
|
command git clone https://github.com/zdharma-continuum/zinit "$HOME/.local/share/zinit/zinit.git"
|
|
log_success "zinit installed"
|
|
fi
|
|
fi
|
|
|
|
# Install zoxide (smart cd) if missing
|
|
if ! command -v zoxide &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install zoxide"
|
|
else
|
|
log_info "Installing zoxide..."
|
|
curl -sS https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | bash
|
|
log_success "zoxide installed"
|
|
fi
|
|
fi
|
|
|
|
# Install fzf (fuzzy finder) if missing
|
|
if ! command -v fzf &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install fzf"
|
|
else
|
|
log_info "Installing fzf..."
|
|
if [[ -d "$HOME/.fzf" ]]; then
|
|
"$HOME/.fzf/install" --bin
|
|
else
|
|
git clone --depth 1 https://github.com/junegunn/fzf.git "$HOME/.fzf"
|
|
"$HOME/.fzf/install" --bin
|
|
fi
|
|
ln -sf "$HOME/.fzf/bin/fzf" "$bin_dir/fzf"
|
|
log_success "fzf installed"
|
|
fi
|
|
fi
|
|
|
|
# Install bat (syntax-highlighted cat) if missing
|
|
if ! command -v bat &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install bat"
|
|
else
|
|
log_info "Installing bat..."
|
|
local bat_ver
|
|
bat_ver=$(curl -s https://api.github.com/repos/sharkdp/bat/releases/latest | grep -oP '"tag_name": "\K[^"]+')
|
|
curl -sL "https://github.com/sharkdp/bat/releases/download/${bat_ver}/bat-${bat_ver}-x86_64-unknown-linux-gnu.tar.gz" -o /tmp/bat.tar.gz
|
|
tar xzf /tmp/bat.tar.gz -C /tmp/
|
|
cp "/tmp/bat-${bat_ver}-x86_64-unknown-linux-gnu/bat" "$bin_dir/"
|
|
log_success "bat installed"
|
|
fi
|
|
fi
|
|
|
|
# Install eza (modern ls) if missing
|
|
if ! command -v eza &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install eza"
|
|
else
|
|
log_info "Installing eza..."
|
|
curl -sL "https://github.com/eza-community/eza/releases/latest/download/eza_x86_64-unknown-linux-gnu.tar.gz" -o /tmp/eza.tar.gz
|
|
tar xzf /tmp/eza.tar.gz -C /tmp/
|
|
cp /tmp/eza "$bin_dir/"
|
|
log_success "eza installed"
|
|
fi
|
|
fi
|
|
|
|
# Install delta (better git diff) if missing
|
|
if ! command -v delta &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install delta"
|
|
else
|
|
log_info "Installing delta..."
|
|
local delta_ver
|
|
delta_ver=$(curl -s https://api.github.com/repos/dandavison/delta/releases/latest | grep -oP '"tag_name": "\K[^"]+')
|
|
curl -sL "https://github.com/dandavison/delta/releases/download/${delta_ver}/delta-${delta_ver}-x86_64-unknown-linux-gnu.tar.gz" -o /tmp/delta.tar.gz
|
|
tar xzf /tmp/delta.tar.gz -C /tmp/
|
|
cp "/tmp/delta-${delta_ver}-x86_64-unknown-linux-gnu/delta" "$bin_dir/"
|
|
log_success "delta installed"
|
|
fi
|
|
fi
|
|
|
|
|
|
# Install starship (cross-shell prompt) if missing
|
|
if ! command -v starship &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install starship"
|
|
else
|
|
log_info "Installing starship..."
|
|
curl -sS https://starship.rs/install.sh | sh -s -- --force --bin-dir "$bin_dir"
|
|
log_success "starship installed"
|
|
fi
|
|
fi
|
|
|
|
# Install ripgrep (fast grep alternative) if missing
|
|
if ! command -v rg &> /dev/null; then
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would install ripgrep"
|
|
else
|
|
log_info "Installing ripgrep..."
|
|
local rg_ver
|
|
rg_ver=$(curl -s https://api.github.com/repos/BurntSushi/ripgrep/releases/latest | grep -oP '"tag_name": "\K[^"]+')
|
|
curl -sL "https://github.com/BurntSushi/ripgrep/releases/download/${rg_ver}/ripgrep-${rg_ver}-x86_64-unknown-linux-musl.tar.gz" -o /tmp/ripgrep.tar.gz
|
|
tar xzf /tmp/ripgrep.tar.gz -C /tmp/
|
|
cp "/tmp/ripgrep-${rg_ver}-x86_64-unknown-linux-musl/rg" "$bin_dir/"
|
|
log_success "ripgrep installed"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
declare -A BACKUP_PATTERNS
|
|
BACKUP_PATTERNS[ssh]=".ssh/config .ssh/known_hosts"
|
|
|
|
should_backup_file() {
|
|
local package=$1
|
|
local relative_path=$2
|
|
|
|
if [[ -n "${BACKUP_PATTERNS[$package]:-}" ]]; then
|
|
local patterns="${BACKUP_PATTERNS[$package]}"
|
|
for pattern in $patterns; do
|
|
if [[ "$relative_path" == "$pattern" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
fi
|
|
|
|
case "$relative_path" in
|
|
*.gradle/caches/* | *.gradle/daemon/* | *.gradle/wrapper/* | \
|
|
*.gradle/.tmp/* | *.gradle/native/* | *.gradle/jdks/* | \
|
|
*.gradle/notifications/* | *.gradle/workers/* | \
|
|
*.gradle/kotlin-profile/* | *.gradle/android/* | \
|
|
*.gradle/*.lock | *.cache/* | */.DS_Store | \
|
|
*/node_modules/* | */build/* | */dist/* | \
|
|
*/__pycache__/* | *.pyc | *.pyo | \
|
|
*.log | *.tmp | *.temp)
|
|
return 1
|
|
;;
|
|
*)
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
backup_existing() {
|
|
local package=$1
|
|
local files_backed_up=0
|
|
local files_skipped=0
|
|
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
log_info "Checking for existing files for package: $package"
|
|
fi
|
|
|
|
while IFS= read -r -d '' file; do
|
|
local relative_path="${file#$SCRIPT_DIR/$package/}"
|
|
local target_path="$HOME/$relative_path"
|
|
|
|
if [[ -e "$target_path" ]] && [[ ! -L "$target_path" ]]; then
|
|
if should_backup_file "$package" "$relative_path"; then
|
|
if [[ "$files_backed_up" -eq 0 ]]; then
|
|
mkdir -p "$BACKUP_DIR"
|
|
log_info "Backing up existing files to $BACKUP_DIR"
|
|
fi
|
|
|
|
local backup_path="$BACKUP_DIR/$relative_path"
|
|
mkdir -p "$(dirname "$backup_path")"
|
|
cp -r "$target_path" "$backup_path"
|
|
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
log_info "Backed up: $target_path -> $backup_path"
|
|
fi
|
|
((files_backed_up++))
|
|
else
|
|
if [[ "$VERBOSE" == "true" ]]; then
|
|
log_info "Skipped backup (generated/cache): $target_path"
|
|
fi
|
|
((files_skipped++))
|
|
fi
|
|
fi
|
|
done < <(find "$SCRIPT_DIR/$package" -type f -print0 2>/dev/null || true)
|
|
|
|
if [[ "$files_backed_up" -gt 0 ]]; then
|
|
log_success "Backed up $files_backed_up files for $package"
|
|
fi
|
|
|
|
if [[ "$files_skipped" -gt 0 ]] && [[ "$VERBOSE" == "true" ]]; then
|
|
log_info "Skipped $files_skipped generated/cache files"
|
|
fi
|
|
}
|
|
|
|
clean_generated_files() {
|
|
local package=$1
|
|
|
|
log_info "Cleaning generated files for $package..."
|
|
|
|
case "$package" in
|
|
ssh)
|
|
find "$SCRIPT_DIR/$package/.ssh" -name "*.backup" -o -name "*.tmp" -o -name "*.old" \
|
|
-exec rm -f {} + 2>/dev/null || true
|
|
;;
|
|
*)
|
|
find "$SCRIPT_DIR/$package" -name "*.log" -o -name "*.tmp" -o -name "*.backup" \
|
|
-exec rm -f {} + 2>/dev/null || true
|
|
;;
|
|
esac
|
|
}
|
|
|
|
install_package() {
|
|
local package=$1
|
|
|
|
if [[ ! -d "$SCRIPT_DIR/$package" ]]; then
|
|
log_error "Package '$package' not found in $SCRIPT_DIR"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Installing $package configuration..."
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "[DRY RUN] Would clean generated files for $package"
|
|
log_info "[DRY RUN] Would run: stow $package"
|
|
log_info "[DRY RUN] Would link files from $SCRIPT_DIR/$package to $HOME"
|
|
return 0
|
|
fi
|
|
|
|
clean_generated_files "$package"
|
|
|
|
backup_existing "$package"
|
|
|
|
if stow "$package"; then
|
|
log_success "Successfully installed $package"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Parse command line arguments
|
|
DRY_RUN=false
|
|
VERBOSE=false
|
|
QUIET=false
|
|
SELECTED_PACKAGES=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
-d|--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
-v|--verbose)
|
|
VERBOSE=true
|
|
shift
|
|
;;
|
|
-q|--quiet)
|
|
QUIET=true
|
|
shift
|
|
;;
|
|
--only=*)
|
|
IFS=',' read -ra SELECTED_PACKAGES <<< "${1#*=}"
|
|
shift
|
|
;;
|
|
--backup-dir=*)
|
|
BACKUP_DIR="${1#*=}"
|
|
shift
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
show_help
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#SELECTED_PACKAGES[@]} -eq 0 ]]; then
|
|
PACKAGES_TO_INSTALL=("${AVAILABLE_PACKAGES[@]}")
|
|
else
|
|
PACKAGES_TO_INSTALL=("${SELECTED_PACKAGES[@]}")
|
|
for package in "${PACKAGES_TO_INSTALL[@]}"; do
|
|
if [[ ! " ${AVAILABLE_PACKAGES[*]} " =~ " ${package} " ]]; then
|
|
log_error "Unknown package: $package"
|
|
log_info "Available packages: ${AVAILABLE_PACKAGES[*]}"
|
|
exit 1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [[ "$QUIET" == "true" ]]; then
|
|
exec > /dev/null 2>&1
|
|
fi
|
|
|
|
main() {
|
|
log_info "Starting dotfiles installation..."
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then
|
|
log_info "DRY RUN MODE - No changes will be made"
|
|
fi
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
check_dependencies
|
|
|
|
install_toolchain
|
|
|
|
local failed_packages=()
|
|
|
|
for package in "${PACKAGES_TO_INSTALL[@]}"; do
|
|
if ! install_package "$package"; then
|
|
failed_packages+=("$package")
|
|
fi
|
|
done
|
|
|
|
echo
|
|
if [[ ${#failed_packages[@]} -eq 0 ]]; then
|
|
log_success "All packages installed successfully!"
|
|
if [[ -d "$BACKUP_DIR" ]]; then
|
|
log_info "Backup created at: $BACKUP_DIR"
|
|
fi
|
|
else
|
|
log_error "Some packages failed to install: ${failed_packages[*]}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
main
|