aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CLAUDE.md36
-rw-r--r--README.md123
-rw-r--r--config.example11
-rwxr-xr-xdot-backup.sh247
4 files changed, 371 insertions, 46 deletions
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..c55bd86
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,36 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What This Is
+
+Single-script dotfile backup tool. `dot-backup.sh` copies files from `DOTFILES` array to a local git repo (`~/Programming/GIT/my-dotfiles`), split into `home/` and `system/` subdirectories, then stages everything with `git add`.
+
+## Running
+
+```bash
+./dot-backup.sh
+```
+
+No build step. No tests. No deps beyond bash + rsync + git.
+
+## Architecture
+
+`dot-backup.sh` does three things in sequence:
+
+1. **Setup** — ensures `$DEFAULT_OUTPUT_DIR` exists with `home/` and `system/` subdirs; initializes git if needed; writes `lastupdate` timestamp
+2. **Copy loop** — iterates `DOTFILES` array; paths starting with `/` go to `system/`, all others go to `home/`; uses `rsync -a` for both files and directories
+3. **Stage** — `git add .` in `$DEFAULT_OUTPUT_DIR`; user must commit/push manually
+
+## Key Variables
+
+| Variable | Default | Purpose |
+|---|---|---|
+| `DEFAULT_OUTPUT_DIR` | `~/Programming/GIT/my-dotfiles` | Where backups land |
+| `DOTFILES` | array in script | What gets backed up |
+
+To change backup destination, edit `DEFAULT_OUTPUT_DIR` at the top of the script. A commented-out `/tmp/` alternative is already there for testing.
+
+## Adding Dotfiles
+
+Add paths to the `DOTFILES` array. Relative paths = home dir. Absolute paths (starting `/`) = system files (copied preserving full path under `system/`).
diff --git a/README.md b/README.md
index 140ae2c..1550e51 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,124 @@
# dots-backup
-A simple bash script to backup all my dotfiles and have them available to be easily restored on any system. \ No newline at end of file
+Bash script to back up dotfiles into a local git repo, and restore them on any system.
+
+## Requirements
+
+- `bash`
+- `rsync`
+- `git`
+
+## How It Works
+
+`dot-backup.sh` reads a list of dotfiles, copies them into a local git repo using `rsync`, then auto-commits any changes.
+
+Backup layout:
+
+```
+~/Programming/GIT/my-dotfiles/
+├── home/ # files from $HOME (relative paths)
+│ ├── .bashrc
+│ ├── .gitconfig
+│ └── .config/
+│ └── nvim/
+└── system/ # files from / (absolute paths)
+ └── etc/
+ └── bash_completion.d/
+```
+
+## First Run
+
+```bash
+git clone https://github.com/you/dots-backup
+cd dots-backup
+./dot-backup.sh
+```
+
+On first run the script creates `~/Programming/GIT/my-dotfiles` and initializes a git repo inside it. Add a remote so you can push:
+
+```bash
+cd ~/Programming/GIT/my-dotfiles
+git remote add origin git@github.com:you/my-dotfiles.git
+git push -u origin master
+```
+
+## Usage
+
+```
+./dot-backup.sh [options]
+
+ -n, --dry-run Show what would be copied without copying
+ -v, --verbose Print each file as rsync transfers it
+ -r, --restore Restore dotfiles from backup to original locations
+ -q, --quiet Suppress stdout; write output to log instead
+ -h, --help Show this help
+```
+
+## Daily Routine
+
+Run manually after changing config:
+
+```bash
+./dot-backup.sh
+```
+
+Or automate with cron (silent, logs to `~/.local/share/dot-backup/backup.log`):
+
+```bash
+# crontab -e
+0 * * * * /path/to/dot-backup.sh -q
+```
+
+After backup, push manually:
+
+```bash
+cd ~/Programming/GIT/my-dotfiles && git push
+```
+
+## Restoring on a New Machine
+
+Preview what would be restored first:
+
+```bash
+./dot-backup.sh -r --dry-run
+```
+
+Then restore:
+
+```bash
+./dot-backup.sh -r
+```
+
+System files (absolute paths like `/etc/bash_completion.d`) may need `sudo`.
+
+## Configuration
+
+Copy `config.example` to `~/.config/dot-backup/config` and edit:
+
+```bash
+mkdir -p ~/.config/dot-backup
+cp config.example ~/.config/dot-backup/config
+```
+
+Available options:
+
+```bash
+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"
+```
+
+## Adding Files
+
+**Edit the script** — add paths to the `DOTFILES` array in `dot-backup.sh`.
+
+**Or use the external list** — add paths to `~/.config/dot-backup/files.list`, one per line:
+
+```
+# extra dotfiles
+.config/myapp
+.config/otherapp.conf
+/etc/hosts
+```
+
+Relative paths are treated as `$HOME`-relative. Absolute paths (starting with `/`) are treated as system files.
diff --git a/config.example b/config.example
new file mode 100644
index 0000000..f4b98f5
--- /dev/null
+++ b/config.example
@@ -0,0 +1,11 @@
+# dot-backup configuration
+# Copy to ~/.config/dot-backup/config and edit as needed
+
+# Where backups are stored
+#DEFAULT_OUTPUT_DIR="${HOME}/Programming/GIT/my-dotfiles"
+
+# Log file location (used with -q/--quiet)
+#LOG_FILE="${HOME}/.local/share/dot-backup/backup.log"
+
+# External dotfiles list (one path per line, # for comments)
+#DOTFILES_LIST="${HOME}/.config/dot-backup/files.list"
diff --git a/dot-backup.sh b/dot-backup.sh
index aef6fee..6813b14 100755
--- a/dot-backup.sh
+++ b/dot-backup.sh
@@ -1,12 +1,12 @@
#! /bin/bash
#####################################################################
-# ___ __ ___ __
-# __| _/ ____ _/ |_ \_ |__ _____ ____ | | ____ ________
-# / __ | / __ \\ __\ | __ \\__ \ _/ ___\| |/ / | \____ \
+# ___ __ ___ __
+# __| _/ ____ _/ |_ \_ |__ _____ ____ | | ____ ________
+# / __ | / __ \\ __\ | __ \\__ \ _/ ___\| |/ / | \____ \
# / /_/ |( \_\ )| | | \_\ \/ __ \_ \___| \| | / |_\ \
# \____ | \____/ |__| |___ /____ /\___ /__|_ \____/| ___/
-# \/ \/ \/ \/ \/ |__|
+# \/ \/ \/ \/ \/ |__|
#
# by danix
#
@@ -23,9 +23,53 @@ NC='\033[0m' # No Color
### DIRECTORIES
WORKDIR=$(pwd)
-# Output directory
-#DEFAULT_OUTPUT_DIR="${HOME}/Programming/GIT/my-dotfiles"
-DEFAULT_OUTPUT_DIR="/tmp/my-dotfiles"
+
+# 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"
+
+# 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
+
+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 " -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 ;;
+ -h|--help) usage ;;
+ *) echo -e "${RED}Unknown option: $arg${NC}"; usage ;;
+ esac
+done
+
+if [[ "$QUIET" == true ]]; then
+ mkdir -p "$(dirname "$LOG_FILE")"
+ # 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=(
@@ -104,33 +148,137 @@ DOTFILES=(
"/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() {
- case $2 in
+beginswith() {
+ case $2 in
"$1"*)
true
- ;;
+ ;;
*)
false
;;
esac;
}
-# clear the screen before we output anything
+lastupdate() {
+ D=$(date)
+ [ ! -f ${DEFAULT_OUTPUT_DIR}/lastupdate ] && touch ${DEFAULT_OUTPUT_DIR}/lastupdate
+ echo "Last update: $D" > ${DEFAULT_OUTPUT_DIR}/lastupdate
+}
+
+log_verbose() {
+ [[ "$VERBOSE" == true ]] && echo -e "$1"
+}
+
+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"
+
+ for i in "${DOTFILES[@]}"; do
+ if beginswith "/" "$i"; then
+ # system file
+ local backup_path="${DEFAULT_OUTPUT_DIR}/system/${i#/}"
+ local dest="$i"
+ 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 [[ "$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 ──────────────────────────────────────────────────────────────────────
+
clear
+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}"
- # it exists, so we check for subdirectories
- # - home
- # - system
+ 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
- # check if we are in a git repository
cd $DEFAULT_OUTPUT_DIR
if [[ $(git rev-parse --is-inside-work-tree) == "true" ]]; then
- # we are inside a git repo
echo -e "${GREEN}Git Repository already initialized.${NC}"
else
echo -e "${YELLOW}Initializing Git Repository. Don't forget to add your remotes.${NC}"
@@ -138,55 +286,64 @@ if [[ -d $DEFAULT_OUTPUT_DIR ]]; then
git add .
fi
else
- echo -e "\t${BLUE}$DEFAULT_OUTPUT_DIR ${RED} ${NC}"
- # our backup directory doesn't exists, so we make it
- echo -e "${YELLOW}creating our backup directories${NC}"
- mkdir -p $DEFAULT_OUTPUT_DIR/{home,system}
- echo -e "${YELLOW}Initializing Git Repository. Don't forget to add your remotes.${NC}"
- cd $DEFAULT_OUTPUT_DIR
- git init
- git add .
+ 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. Don't forget to add your remotes.${NC}"
+ cd $DEFAULT_OUTPUT_DIR
+ git init
+ git add .
+ fi
fi
-# reset colors
+if [[ "$DRY_RUN" == false ]]; then
+ lastupdate
+fi
echo -e $NC
cd $WORKDIR
# we iterate all dotfiles in the list
-for i in ${DOTFILES[@]}; do
+for i in "${DOTFILES[@]}"; do
# if it's a file in my home
if [[ -f ${HOME}/$i ]]; then
- echo -e "${GREEN}${HOME}/$i\t  ${NC}"
- # it exists, we copy it to our output directory
- rsync -a ${HOME}/$i ${DEFAULT_OUTPUT_DIR}/home/ && echo -e "\t ${GREEN}Copied\t  ${NC}"
+ 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\t  ${NC}"
- # it exists, we copy it to our output directory
- rsync -a ${HOME}/$i ${DEFAULT_OUTPUT_DIR}/home/ && echo -e "\t ${GREEN}Copied\t  ${NC}"
- # if it begins with a / it's a system file/directory
+ 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 it's a file
if [[ -f $i ]]; then
- echo -e "${GREEN}$i\t  ${NC}"
- # it exists, we copy it to our output directory
- rsync -a $i ${DEFAULT_OUTPUT_DIR}/system/ && echo -e "\t ${GREEN}Copied\t  ${NC}"
- # if it's a directory
+ echo -e "${GREEN}$i${NC}"
+ do_rsync "$i" "${DEFAULT_OUTPUT_DIR}/system/$i"
elif [[ -d $i ]]; then
- echo -e "${BLUE}$i\t  ${NC}"
- # it exists, we copy it to our output directory
- rsync -a $i ${DEFAULT_OUTPUT_DIR}/system/ && echo -e "\t ${GREEN}Copied\t  ${NC}"
+ echo -e "${BLUE}$i${NC}"
+ do_rsync "$i" "${DEFAULT_OUTPUT_DIR}/system/${i#/}" true
fi
- # if it doesn't exists
else
- echo -e "\n${RED}NOT FOUND: ${i}\t  ${NC}\n"
+ 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
git add .
-# reset colors before exiting
+
+if git diff --cached --quiet; then
+ echo -e "${YELLOW}Nothing new to commit.${NC}"
+else
+ git commit -m "backup: $(date '+%Y-%m-%d %H:%M:%S')"
+ echo -e "${GREEN}Committed.${NC}"
+fi
+
echo -e $NC
cd $WORKDIR
+if [[ "$QUIET" == true ]]; then echo "Log written to: $LOG_FILE" >&3; fi
exit 0