aboutsummaryrefslogtreecommitdiffstats
path: root/dot-backup.sh
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 /dot-backup.sh
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>
Diffstat (limited to 'dot-backup.sh')
-rwxr-xr-xdot-backup.sh113
1 files changed, 112 insertions, 1 deletions
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