From c58978b328b1f9086b3bb09bbe4c19312c8bc0f8 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:32:22 +0200 Subject: feat: add --suggest flag plumbing Adds the SUGGEST flag variable, CLI argument parsing, and help text for the upcoming --suggest feature. The flag is wired but behavior will be implemented in subsequent tasks. Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dot-backup.sh b/dot-backup.sh index 701153f..128a733 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -52,6 +52,7 @@ VERBOSE=false RESTORE=false QUIET=false PUSH=false +SUGGEST=false usage() { echo "Usage: $0 [options]" @@ -60,6 +61,7 @@ usage() { 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 " -s, --suggest List untracked config dirs/files new since last backup, then exit" echo " -h, --help Show this help" exit 0 } @@ -71,6 +73,7 @@ for arg in "$@"; do -r|--restore) RESTORE=true ;; -q|--quiet) QUIET=true ;; -p|--push) PUSH=true ;; + -s|--suggest) SUGGEST=true ;; -h|--help) usage ;; *) echo -e "${RED}Unknown option: $arg${NC}"; usage ;; esac -- cgit v1.2.3 From c276b82f71d6d0494cae8ff19633e8880a98ccfd Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:34:16 +0200 Subject: feat: suggest bypasses --quiet redirect Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dot-backup.sh b/dot-backup.sh index 128a733..463f783 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -79,7 +79,7 @@ for arg in "$@"; do esac done -if [[ "$QUIET" == true ]]; then +if [[ "$QUIET" == true && "$SUGGEST" == false ]]; then # save original stdout so we can notify user after redirect exec 3>&1 # strip color codes and write to log -- cgit v1.2.3 From f4f80e62c997dbe185337aa1a18509b125759875 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:36:13 +0200 Subject: feat: lastupdate also writes machine-readable epoch Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/dot-backup.sh b/dot-backup.sh index 463f783..567d6c8 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -187,6 +187,7 @@ beginswith() { [[ "$2" == "$1"* ]]; } lastupdate() { D=$(date) echo "Last update: $D" > "${DEFAULT_OUTPUT_DIR}/lastupdate" + date +%s > "${DEFAULT_OUTPUT_DIR}/lastupdate.epoch" } log_verbose() { -- cgit v1.2.3 From 7c99dbace31d9694e29bdf69f502f2add1968fda Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:37:33 +0200 Subject: feat: add SUGGEST_IGNORE default skip list Add guarded default for SUGGEST_IGNORE array containing 16 common untracked basenames (.cache, .local, .git, .ssh, .gnupg, .Trash, .pki, .nv, .mozilla, .thunderbird, .npm, .cargo, .rustup, .java, .dbus, .config). User can override in config file - ${SUGGEST_IGNORE+set} guard ensures config values win. Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dot-backup.sh b/dot-backup.sh index 567d6c8..2ffdbcc 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -46,6 +46,13 @@ if [[ -f "$CONFIG_FILE" ]]; then source "$CONFIG_FILE" fi +# Basenames skipped by --suggest. User may override in config. +if [[ -z "${SUGGEST_IGNORE+set}" ]]; then + SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \ + .mozilla .thunderbird .npm .cargo .rustup .java \ + .dbus .config) +fi + # Flags DRY_RUN=false VERBOSE=false -- cgit v1.2.3 From b3568ae80439223b3b313bdc3bfad4edc5c7f939 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:39:40 +0200 Subject: feat: add do_suggest scan/filter/dedup function Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/dot-backup.sh b/dot-backup.sh index 2ffdbcc..21388f0 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -226,6 +226,90 @@ do_rsync() { fi } +# ── SUGGEST MODE ────────────────────────────────────────────────────────────── + +# True if candidate path $1 is already covered by any DOTFILES entry +# (exact match, parent of, or child of an existing entry). +_suggest_covered() { + local c="$1" e + for e in "${DOTFILES[@]}"; do + # normalize trailing slash + e="${e%/}" + [[ "$c" == "$e" ]] && return 0 + [[ "$c" == "$e"/* ]] && return 0 + [[ "$e" == "$c"/* ]] && return 0 + done + return 1 +} + +# True if basename $1 is in SUGGEST_IGNORE +_suggest_ignored() { + local b="$1" ig + for ig in "${SUGGEST_IGNORE[@]}"; do + [[ "$b" == "$ig" ]] && return 0 + done + return 1 +} + +do_suggest() { + local epoch_file="${DEFAULT_OUTPUT_DIR}/lastupdate.epoch" + local last_epoch=0 + local last_human="never" + if [[ -r "$epoch_file" ]]; then + local raw + raw="$(cat "$epoch_file")" + if [[ "$raw" =~ ^[0-9]+$ ]]; then + last_epoch="$raw" + last_human="$(date -d "@$last_epoch" 2>/dev/null || echo "$last_epoch")" + fi + fi + + local -a candidates=() + local path emit base mtime + + # Source 1: ~/.config/* (children of .config) + for path in "${HOME}/.config/"*; do + [[ -e "$path" ]] || continue + base="$(basename "$path")" + emit=".config/${base}" + _suggest_ignored "$base" && continue + _suggest_covered "$emit" && continue + mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" + (( mtime > last_epoch )) && candidates+=("$emit") + done + + # Source 2 + 3: ~/.* (hidden dirs and files at home top level) + for path in "${HOME}/".*; do + base="$(basename "$path")" + [[ "$base" == "." || "$base" == ".." ]] && continue + [[ -d "$path" || -f "$path" ]] || continue + emit="${base}" + _suggest_ignored "$base" && continue + _suggest_covered "$emit" && continue + mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" + (( mtime > last_epoch )) && candidates+=("$emit") + done + + if [[ ${#candidates[@]} -eq 0 ]]; then + echo -e "${GREEN}No new config dirs/files since last backup.${NC}" + return 0 + fi + + # sort alphabetically, stable output + local -a sorted=() + while IFS= read -r line; do sorted+=("$line"); done < <(printf '%s\n' "${candidates[@]}" | sort -u) + + echo -e "${GREEN}New since last backup (${last_human}):${NC}" + for emit in "${sorted[@]}"; do + echo -e " ${BLUE}${emit}${NC}" + done + echo + echo "Paste into files.list:" + for emit in "${sorted[@]}"; do + echo "$emit" + done +} + # ── RESTORE MODE ────────────────────────────────────────────────────────────── do_restore() { -- cgit v1.2.3 From 3aaaf1753d8aa444fda44064ff2c655c5788cb25 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:46:06 +0200 Subject: refactor: clearer arithmetic guard and output loop var in do_suggest Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dot-backup.sh b/dot-backup.sh index 21388f0..7c77531 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -265,7 +265,7 @@ do_suggest() { fi local -a candidates=() - local path emit base mtime + local path emit base mtime item # Source 1: ~/.config/* (children of .config) for path in "${HOME}/.config/"*; do @@ -275,7 +275,7 @@ do_suggest() { _suggest_ignored "$base" && continue _suggest_covered "$emit" && continue mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" - (( mtime > last_epoch )) && candidates+=("$emit") + if (( mtime > last_epoch )); then candidates+=("$emit"); fi done # Source 2 + 3: ~/.* (hidden dirs and files at home top level) @@ -287,7 +287,7 @@ do_suggest() { _suggest_ignored "$base" && continue _suggest_covered "$emit" && continue mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" - (( mtime > last_epoch )) && candidates+=("$emit") + if (( mtime > last_epoch )); then candidates+=("$emit"); fi done if [[ ${#candidates[@]} -eq 0 ]]; then @@ -300,13 +300,13 @@ do_suggest() { while IFS= read -r line; do sorted+=("$line"); done < <(printf '%s\n' "${candidates[@]}" | sort -u) echo -e "${GREEN}New since last backup (${last_human}):${NC}" - for emit in "${sorted[@]}"; do - echo -e " ${BLUE}${emit}${NC}" + for item in "${sorted[@]}"; do + echo -e " ${BLUE}${item}${NC}" done echo echo "Paste into files.list:" - for emit in "${sorted[@]}"; do - echo "$emit" + for item in "${sorted[@]}"; do + echo "$item" done } -- cgit v1.2.3 From f90d7f606aca51235fdb2a22fdf2263218138331 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:48:04 +0200 Subject: feat: dispatch --suggest early and exit Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dot-backup.sh b/dot-backup.sh index 7c77531..e95c67f 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -403,6 +403,11 @@ if [[ "$FIRST_RUN" == true ]]; then exit 0 fi +if [[ "$SUGGEST" == true ]]; then + do_suggest + exit 0 +fi + if [[ "$DRY_RUN" == true ]]; then echo -e "${YELLOW}*** DRY RUN — no files will be copied ***${NC}\n" fi -- cgit v1.2.3 From db404714f29ca0230e1a2ba48f7fdc23ff511dbb Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:50:56 +0200 Subject: docs: document --suggest flag and SUGGEST_IGNORE Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 +++- README.md | 6 ++++++ config.example | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index d5c7650..1a4aed5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,10 +18,12 @@ No build step. No tests. No deps beyond bash + rsync + git. `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 +1. **Setup** — ensures `$DEFAULT_OUTPUT_DIR` exists with `home/` and `system/` subdirs; initializes git if needed; writes `lastupdate` timestamp and `lastupdate.epoch` (epoch seconds, machine-readable) 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. **Commit** — `git add .` + auto-commits with timestamp message in `$DEFAULT_OUTPUT_DIR`; optionally pushes with `--push` +`--suggest` is a read-only shortcut: it reads `lastupdate.epoch`, scans `~/.config` children and top-level hidden dirs/files in `$HOME`, and prints any that are newer than the last backup and not already tracked, formatted as a paste-ready block for `files.list`. Prints and exits — never copies or commits. Basenames to skip are controlled by `SUGGEST_IGNORE` (baked-in default; override in config). + ## Key Variables | Variable | Default | Purpose | diff --git a/README.md b/README.md index d75e366..1a7ea3b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ git push -u origin master -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 + -s, --suggest List untracked config dirs/files new since last backup, then exit -p, --push Push to remote after commit -h, --help Show this help ``` @@ -129,10 +130,13 @@ LOG_FILE="${HOME}/.local/share/dot-backup/backup.log" DOTFILES_LIST="${HOME}/.config/dot-backup/files.list" GIT_REMOTE="git@github.com:you/my-dotfiles.git" GIT_BRANCH="master" +SUGGEST_IGNORE=(.cache .local .git .ssh ...) ``` `GIT_REMOTE` — if set, script ensures it is registered as `origin` on every run. Required for `--push`. `GIT_BRANCH` defaults to the current branch if unset. +`SUGGEST_IGNORE` — basenames `--suggest` skips when scanning for new config (overrides the baked-in default). See `config.example` for the full default list. + ## Adding Files **External list (recommended)** — create `~/.config/dot-backup/files.list` with one path per line. When this file exists and is non-empty it replaces the built-in list entirely: @@ -150,3 +154,5 @@ GIT_BRANCH="master" **Built-in fallback** — if `files.list` is missing or empty, the script uses the `DOTFILES` array hardcoded in `dot-backup.sh`. Relative paths are treated as `$HOME`-relative. Absolute paths (starting with `/`) are treated as system files. + +**Discovering new files** — `--suggest` scans `~/.config` children and top-level hidden dirs/files in `$HOME`, and prints any that are newer than the last backup and not already tracked, formatted for pasting into `files.list`. It never copies or commits. Tune what it skips with `SUGGEST_IGNORE` in the config file. diff --git a/config.example b/config.example index 79e716d..cbd419e 100644 --- a/config.example +++ b/config.example @@ -15,3 +15,9 @@ # Branch to push to when using -p/--push (defaults to current branch if unset) #GIT_BRANCH="master" + +# Basenames --suggest skips when scanning for new config (override the +# baked-in default). Uncomment and edit to customize. +#SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \ +# .mozilla .thunderbird .npm .cargo .rustup .java \ +# .dbus .config) -- cgit v1.2.3 From cc576c42c2d81d6201658271541bc13fb4229615 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 16 Jun 2026 18:55:44 +0200 Subject: feat: skip editor/temp junk in --suggest output Vim undo/swap files, backups, *.tmp and .DS_Store matched the home-dir scan and cluttered suggestions. Filter them by suffix in both scan loops. Co-Authored-By: Claude Opus 4.8 --- dot-backup.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dot-backup.sh b/dot-backup.sh index e95c67f..892529c 100755 --- a/dot-backup.sh +++ b/dot-backup.sh @@ -251,6 +251,15 @@ _suggest_ignored() { return 1 } +# True if basename $1 looks like editor/temp junk (vim undo/swap, backups, etc.) +_suggest_junk() { + local b="$1" + case "$b" in + *.un~|*~|*.swp|*.swo|*.tmp|.DS_Store) return 0 ;; + *) return 1 ;; + esac +} + do_suggest() { local epoch_file="${DEFAULT_OUTPUT_DIR}/lastupdate.epoch" local last_epoch=0 @@ -272,6 +281,7 @@ do_suggest() { [[ -e "$path" ]] || continue base="$(basename "$path")" emit=".config/${base}" + _suggest_junk "$base" && continue _suggest_ignored "$base" && continue _suggest_covered "$emit" && continue mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" @@ -284,6 +294,7 @@ do_suggest() { [[ "$base" == "." || "$base" == ".." ]] && continue [[ -d "$path" || -f "$path" ]] || continue emit="${base}" + _suggest_junk "$base" && continue _suggest_ignored "$base" && continue _suggest_covered "$emit" && continue mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)" -- cgit v1.2.3