#! /bin/bash ##################################################################### # ___ __ ___ __ # __| _/ ____ _/ |_ \_ |__ _____ ____ | | ____ ________ # / __ | / __ \\ __\ | __ \\__ \ _/ ___\| |/ / | \____ \ # / /_/ |( \_\ )| | | \_\ \/ __ \_ \___| \| | / |_\ \ # \____ | \____/ |__| |___ /____ /\___ /__|_ \____/| ___/ # \/ \/ \/ \/ \/ |__| # # by danix # ##################################################################### set -euo pipefail # Colors for output GREEN='\033[0;32m' BLUE='\033[0;34m' YELLOW='\033[1;33m' RED='\033[0;31m' NC='\033[0m' # No Color ### DIRECTORIES WORKDIR=$(pwd) # Defaults (can be overridden by config file) DEFAULT_OUTPUT_DIR="${HOME}/Programming/GIT/my-dotfiles" LOG_FILE="${HOME}/.local/share/dot-backup/backup.log" DOTFILES_LIST="${HOME}/.config/dot-backup/files.list" CONFIG_FILE="${HOME}/.config/dot-backup/config" GIT_REMOTE="" GIT_BRANCH="" # Bootstrap config and log directories on first run FIRST_RUN=false if [[ ! -d "$(dirname "$CONFIG_FILE")" ]]; then FIRST_RUN=true fi mkdir -p "$(dirname "$CONFIG_FILE")" mkdir -p "$(dirname "$LOG_FILE")" # Load config file if present if [[ -f "$CONFIG_FILE" ]]; then # shellcheck source=/dev/null source "$CONFIG_FILE" fi # Flags DRY_RUN=false VERBOSE=false RESTORE=false QUIET=false PUSH=false usage() { echo "Usage: $0 [options]" echo " -n, --dry-run Show what would be copied without copying" echo " -v, --verbose Print each file as it is processed" echo " -r, --restore Restore dotfiles from backup to their original locations" echo " -q, --quiet Suppress stdout; write output to log instead" echo " -p, --push Push to remote after commit (requires GIT_REMOTE in config)" echo " -h, --help Show this help" exit 0 } for arg in "$@"; do case $arg in -n|--dry-run) DRY_RUN=true ;; -v|--verbose) VERBOSE=true ;; -r|--restore) RESTORE=true ;; -q|--quiet) QUIET=true ;; -p|--push) PUSH=true ;; -h|--help) usage ;; *) echo -e "${RED}Unknown option: $arg${NC}"; usage ;; esac done if [[ "$QUIET" == true ]]; then # save original stdout so we can notify user after redirect exec 3>&1 # strip color codes and write to log exec > >(sed 's/\x1b\[[0-9;]*m//g' > "$LOG_FILE") 2>&1 fi # The list of dotfiles DOTFILES=( # Shell configurations ".bashrc" ".bash_profile" ".bash_aliases" ".bash_functions" ".dir_colors" ".lessfilter" ".profile.d" # Shell history (optional - comment out if you don't want history) # ".bash_history" # Git configuration ".gitconfig" ".gitignore_global" ".gitmessage" # Desktop ".fonts" ".icons" ".local/share/applications" ".local/share/icons" ".user-icon.jpg" # Editors ".config/nvim" ".config/sublime-text" # hyprland ".config/hypr" ".config/wal" ".config/waybar" ".config/qarma.conf" # firefox # ".mozilla" # thunderbird # ".thunderbird" # music players ".config/mpd" ".config/rmpc" ".config/sayonara" ".config/haruna" ".config/harunarc" # other programs ".config/bash-notes.rc" ".config/cherrytree" ".config/conky" ".config/draw.io" ".config/featherpad" ".config/htop" ".config/joplin-desktop" ".config/kitty" ".config/lximage-qt" ".config/lxqt" ".config/mimeapps.list" ".config/pcmanfm-qt" ".config/powerline" ".config/qpdfview" ".config/rofi" # pandoc ".local/share/pandoc/" # my executables "bin" ### SYSTEM FILES # bash completion "/etc/bash_completion.d" ) # Load external list if it exists, appending to built-in list if [[ -f "$DOTFILES_LIST" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue DOTFILES+=("$line") done < "$DOTFILES_LIST" fi # helper function to check if a string ($2) begins with another string ($1) beginswith() { [[ "$2" == "$1"* ]]; } lastupdate() { D=$(date) echo "Last update: $D" > "${DEFAULT_OUTPUT_DIR}/lastupdate" } log_verbose() { [[ "$VERBOSE" == true ]] && echo -e "$1" || true } do_rsync() { local src="$1" local dst="$2" local is_dir="${3:-false}" if [[ "$DRY_RUN" == true ]]; then echo -e "\t ${YELLOW}[dry-run] would copy: $src → $dst${NC}" else if [[ "$is_dir" == true ]]; then mkdir -p "$dst" if [[ "$VERBOSE" == true ]]; then rsync -av "$src/" "$dst/" else rsync -a "$src/" "$dst/" && echo -e "\t ${GREEN}Copied${NC}" fi else if [[ "$VERBOSE" == true ]]; then rsync -av "$src" "$dst" else rsync -a "$src" "$dst" && echo -e "\t ${GREEN}Copied${NC}" fi fi fi } # ── RESTORE MODE ────────────────────────────────────────────────────────────── do_restore() { if [[ ! -d "$DEFAULT_OUTPUT_DIR" ]]; then echo -e "${RED}Backup directory not found: $DEFAULT_OUTPUT_DIR${NC}" exit 1 fi echo -e "${YELLOW}Restoring dotfiles from: ${BLUE}$DEFAULT_OUTPUT_DIR${NC}\n" local system_skipped=0 for i in "${DOTFILES[@]}"; do if beginswith "/" "$i"; then # system file — requires root local backup_path="${DEFAULT_OUTPUT_DIR}/system/${i#/}" local dest="$i" if [[ "$EUID" -ne 0 ]]; then if [[ -e "$backup_path" ]]; then if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}$dest${NC}" echo -e "\t ${YELLOW}[dry-run] would restore: $backup_path → $dest (needs root)${NC}" else echo -e "${YELLOW}SKIP (needs root): $dest${NC}" (( system_skipped++ )) || true fi else log_verbose "\t ${YELLOW}SKIP (not in backup): $i${NC}" fi continue fi else local backup_path="${DEFAULT_OUTPUT_DIR}/home/$i" local dest="${HOME}/$i" fi if [[ -f "$backup_path" ]]; then echo -e "${GREEN}$dest${NC}" if [[ "$DRY_RUN" == true ]]; then echo -e "\t ${YELLOW}[dry-run] would restore: $backup_path → $dest${NC}" else mkdir -p "$(dirname "$dest")" if [[ "$VERBOSE" == true ]]; then rsync -av "$backup_path" "$dest" else rsync -a "$backup_path" "$dest" && echo -e "\t ${GREEN}Restored${NC}" fi fi elif [[ -d "$backup_path" ]]; then echo -e "${BLUE}$dest${NC}" if [[ "$DRY_RUN" == true ]]; then echo -e "\t ${YELLOW}[dry-run] would restore: $backup_path/ → $dest/${NC}" else mkdir -p "$dest" if [[ "$VERBOSE" == true ]]; then rsync -av "$backup_path/" "$dest/" else rsync -a "$backup_path/" "$dest/" && echo -e "\t ${GREEN}Restored${NC}" fi fi else log_verbose "\t ${YELLOW}SKIP (not in backup): $i${NC}" fi done if [[ "$system_skipped" -gt 0 ]]; then echo -e "\n${YELLOW}Warning: $system_skipped system file(s) skipped — re-run as root to restore them.${NC}" fi if [[ "$DRY_RUN" == true ]]; then echo -e "\n${YELLOW}Dry run complete. Nothing restored.${NC}" else echo -e "\n${GREEN}Restore complete.${NC}" fi if [[ "$QUIET" == true ]]; then echo "Log written to: $LOG_FILE" >&3; fi exit 0 } # ── MAIN ────────────────────────────────────────────────────────────────────── if [[ "$QUIET" == false && -t 1 ]]; then clear; fi if [[ "$FIRST_RUN" == true ]]; then echo -e "${GREEN}First run — config directories created:${NC}" echo -e " ${BLUE}$(dirname "$CONFIG_FILE")${NC} — drop config and files.list here" echo -e " ${BLUE}$(dirname "$LOG_FILE")${NC} — logs written here when using -q" echo -e "\nOptional next steps:" echo -e " cp config.example ${CONFIG_FILE}" echo -e " touch ${DOTFILES_LIST}" echo -e "\nRun again to start backing up.\n" exit 0 fi if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}*** DRY RUN — no files will be copied ***${NC}\n" fi if [[ "$RESTORE" == true ]]; then do_restore fi echo -e "${YELLOW}Local Git Repository: ${NC}" if [[ -d "$DEFAULT_OUTPUT_DIR" ]]; then echo -e "\t${BLUE}$DEFAULT_OUTPUT_DIR ${GREEN} ${NC}" [ -d "$DEFAULT_OUTPUT_DIR/home" ] || mkdir -p "$DEFAULT_OUTPUT_DIR/home" [ -d "$DEFAULT_OUTPUT_DIR/system" ] || mkdir -p "$DEFAULT_OUTPUT_DIR/system" cd "$DEFAULT_OUTPUT_DIR" || { echo -e "${RED}Cannot cd to $DEFAULT_OUTPUT_DIR${NC}"; exit 1; } if git rev-parse --is-inside-work-tree &>/dev/null; then echo -e "${GREEN}Git Repository already initialized.${NC}" else echo -e "${YELLOW}Initializing Git Repository.${NC}" git init git add . fi else echo -e "\t${BLUE}$DEFAULT_OUTPUT_DIR ${RED} ${NC}" if [[ "$DRY_RUN" == false ]]; then echo -e "${YELLOW}creating our backup directories${NC}" mkdir -p "$DEFAULT_OUTPUT_DIR"/{home,system} echo -e "${YELLOW}Initializing Git Repository.${NC}" cd "$DEFAULT_OUTPUT_DIR" || { echo -e "${RED}Cannot cd to $DEFAULT_OUTPUT_DIR${NC}"; exit 1; } git init git add . fi fi # Ensure remote is registered if GIT_REMOTE is configured if [[ -n "$GIT_REMOTE" && "$DRY_RUN" == false ]]; then cd "$DEFAULT_OUTPUT_DIR" || { echo -e "${RED}Cannot cd to $DEFAULT_OUTPUT_DIR${NC}"; exit 1; } if git remote get-url origin &>/dev/null; then current=$(git remote get-url origin) if [[ "$current" != "$GIT_REMOTE" ]]; then echo -e "${YELLOW}Updating remote origin: $GIT_REMOTE${NC}" git remote set-url origin "$GIT_REMOTE" fi else echo -e "${YELLOW}Adding remote origin: $GIT_REMOTE${NC}" git remote add origin "$GIT_REMOTE" fi cd "$WORKDIR" fi if [[ "$DRY_RUN" == false ]]; then lastupdate fi echo -e "$NC" # we iterate all dotfiles in the list for i in "${DOTFILES[@]}"; do # if it's a file in my home if [[ -f "${HOME}/$i" ]]; then echo -e "${GREEN}${HOME}/$i${NC}" do_rsync "${HOME}/$i" "${DEFAULT_OUTPUT_DIR}/home/$i" # if it's a directory in my home elif [[ -d "${HOME}/$i" ]]; then echo -e "${BLUE}${HOME}/$i${NC}" do_rsync "${HOME}/$i" "${DEFAULT_OUTPUT_DIR}/home/$i" true # if it begins with a / it's a system file/directory elif beginswith "/" "$i"; then if [[ -f "$i" ]]; then echo -e "${GREEN}$i${NC}" do_rsync "$i" "${DEFAULT_OUTPUT_DIR}/system/${i#/}" elif [[ -d "$i" ]]; then echo -e "${BLUE}$i${NC}" do_rsync "$i" "${DEFAULT_OUTPUT_DIR}/system/${i#/}" true else echo -e "\n${RED}NOT FOUND: ${i}${NC}\n" fi else echo -e "\n${RED}NOT FOUND: ${i}${NC}\n" fi done if [[ "$DRY_RUN" == true ]]; then echo -e "\n${YELLOW}Dry run complete. No files copied, no git changes made.${NC}" exit 0 fi echo -e "\n${GREEN}Adding all copied files to git.${NC}" cd "$DEFAULT_OUTPUT_DIR" || { echo -e "${RED}Cannot cd to $DEFAULT_OUTPUT_DIR${NC}"; exit 1; } git add . if git diff --cached --quiet; then echo -e "${YELLOW}Nothing new to commit.${NC}" else if ! git commit -m "backup: $(date '+%Y-%m-%d %H:%M:%S')"; then echo -e "${RED}Commit failed (hook rejected?). Files staged but not committed.${NC}" exit 1 fi echo -e "${GREEN}Committed.${NC}" fi if [[ "$PUSH" == true ]]; then if ! git remote get-url origin &>/dev/null; then echo -e "${RED}--push requested but no remote configured. Set GIT_REMOTE in config.${NC}" exit 1 fi branch="${GIT_BRANCH:-$(git symbolic-ref --short HEAD)}" echo -e "${GREEN}Pushing to origin/$branch...${NC}" if ! git push origin "$branch"; then echo -e "${RED}Push failed.${NC}" exit 1 fi echo -e "${GREEN}Pushed.${NC}" fi echo -e "$NC" cd "$WORKDIR" if [[ "$QUIET" == true ]]; then echo "Log written to: $LOG_FILE" >&3; fi exit 0