aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-16 18:56:46 +0200
committerDanilo M. <danix@danix.xyz>2026-06-16 18:56:46 +0200
commitdaa2571288e6f3680f2f14dc4f65815179d9641f (patch)
treeb0244bc3bc92ca36aad16a07e2dd488e58311896
parentd9a245ec8c2236e897771459467a8c7c753ce341 (diff)
parentcc576c42c2d81d6201658271541bc13fb4229615 (diff)
downloaddots-backup-daa2571288e6f3680f2f14dc4f65815179d9641f.tar.gz
dots-backup-daa2571288e6f3680f2f14dc4f65815179d9641f.zip
Merge feat/suggest-flag: add --suggest flagHEADmaster
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--CLAUDE.md4
-rw-r--r--README.md6
-rw-r--r--config.example6
-rwxr-xr-xdot-backup.sh113
4 files changed, 127 insertions, 2 deletions
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)
diff --git a/dot-backup.sh b/dot-backup.sh
index 701153f..892529c 100755
--- a/dot-backup.sh
+++ b/dot-backup.sh
@@ -46,12 +46,20 @@ 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
RESTORE=false
QUIET=false
PUSH=false
+SUGGEST=false
usage() {
echo "Usage: $0 [options]"
@@ -60,6 +68,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,12 +80,13 @@ 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
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
@@ -184,6 +194,7 @@ beginswith() { [[ "$2" == "$1"* ]]; }
lastupdate() {
D=$(date)
echo "Last update: $D" > "${DEFAULT_OUTPUT_DIR}/lastupdate"
+ date +%s > "${DEFAULT_OUTPUT_DIR}/lastupdate.epoch"
}
log_verbose() {
@@ -215,6 +226,101 @@ 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
+}
+
+# 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
+ 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 item
+
+ # Source 1: ~/.config/* (children of .config)
+ for path in "${HOME}/.config/"*; do
+ [[ -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)"
+ if (( mtime > last_epoch )); then candidates+=("$emit"); fi
+ 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_junk "$base" && continue
+ _suggest_ignored "$base" && continue
+ _suggest_covered "$emit" && continue
+ mtime="$(stat -c %Y "$path" 2>/dev/null || echo 0)"
+ if (( mtime > last_epoch )); then candidates+=("$emit"); fi
+ 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 item in "${sorted[@]}"; do
+ echo -e " ${BLUE}${item}${NC}"
+ done
+ echo
+ echo "Paste into files.list:"
+ for item in "${sorted[@]}"; do
+ echo "$item"
+ done
+}
+
# ── RESTORE MODE ──────────────────────────────────────────────────────────────
do_restore() {
@@ -308,6 +414,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