diff options
Diffstat (limited to 'dot-backup.sh')
| -rwxr-xr-x | dot-backup.sh | 113 |
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 |
