diff options
| author | Danilo M. <danix@danix.xyz> | 2026-05-06 09:39:58 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-05-06 09:39:58 +0200 |
| commit | 1076091203f9387385e663ad19c7ea3648650bec (patch) | |
| tree | ce57f4dc55f03f0ca3a8102b5de6a3455105eb20 | |
| parent | d446b2521465f11828d172209b61abcb00b18cb4 (diff) | |
| download | dots-backup-1076091203f9387385e663ad19c7ea3648650bec.tar.gz dots-backup-1076091203f9387385e663ad19c7ea3648650bec.zip | |
Add flags, config system, restore mode, and updated docs
- Add -n/--dry-run, -v/--verbose, -r/--restore, -q/--quiet flags
- Auto-commit after backup with timestamp message
- Load ~/.config/dot-backup/config for overriding defaults
- Load extra dotfiles from ~/.config/dot-backup/files.list
- Quiet mode redirects all output to ~/.local/share/dot-backup/backup.log
- Add config.example, CLAUDE.md, and updated README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 36 | ||||
| -rw-r--r-- | README.md | 123 | ||||
| -rw-r--r-- | config.example | 11 | ||||
| -rwxr-xr-x | dot-backup.sh | 247 |
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/`). @@ -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 |
