# Design: `--suggest` flag **Date:** 2026-06-16 **Status:** Approved (pending implementation) ## Goal Add a `-s|--suggest` flag to `dot-backup.sh` that surfaces config directories and files created since the last backup that are not yet tracked in `DOTFILES`, and prints a paste-ready block for `files.list`. The flag is read-only: it never copies, commits, pushes, or mutates any config. It scans, prints, and exits. ## Behavior ### Flag - New flag `-s` / `--suggest`, registered in the arg loop and `usage()`. - When set, run suggest logic, then **exit 0**. No backup/restore/copy/commit. - Mutually exclusive with `--restore` in practice (suggest exits first if both given; suggest takes precedence). ### Scan sources Three sources, all scanned one level deep only: 1. `~/.config/*/` — direct child directories of `~/.config`. Emitted as `.config/`. 2. `~/.*/` — top-level hidden directories in `$HOME`. Emitted as `.`. 3. `~/.*` (files) — top-level hidden regular files in `$HOME`. Emitted as `.`. Always skip the special entries `.` and `..`. ### Candidate filter A scanned path is suggested only if **all** of the following hold: 1. **Newer than last backup** — its mtime is greater than the last-backup timestamp (see "lastupdate change" below). If the timestamp file is missing (never run, or an older backup that predates this feature), treat every path as new. 2. **Not already covered** by a `DOTFILES` entry (see "Dedup matching"). 3. **Not ignored** — its basename is not in `SUGGEST_IGNORE`. ### Dedup matching The emitted path of a candidate is `c` (e.g. `.config/foobar`, `.newtool`). For each existing `DOTFILES` entry `e`, `c` is considered **covered** if any of: - `c == e` (exact match), OR - `c` is under `e/` (e is an ancestor — e.g. `e=.config`, `c=.config/foobar`), OR - `e` is under `c/` (c is an ancestor — e.g. `c=.config`, `e=.config/nvim`). If covered by any entry, the candidate is dropped. This prevents suggesting a path already listed, a path whose parent is listed, or a parent whose child is already listed. Note: `DOTFILES` entries are compared as-emitted (relative home paths and absolute system paths). System entries (starting `/`) never match home-relative candidates, which is correct — suggest only scans `$HOME`. ### SUGGEST_IGNORE A bash array of basenames to skip. If unset (not defined in the config file), the script falls back to a baked-in default: ```bash SUGGEST_IGNORE=(.cache .local .git .ssh .gnupg .Trash .pki .nv \ .mozilla .thunderbird .npm .cargo .rustup .java \ .dbus .config) ``` `.config` is in the list to exclude it from the **home-dir** scan only (its children are scanned separately as source 1). The home-dir dir/file scans check basenames against this list; the `~/.config/*` scan does **not** apply the `.config` skip to its children (only to deeper basenames if present). The user may override by defining `SUGGEST_IGNORE` in `~/.config/dot-backup/config`. A commented example is added to `config.example`. Detection of "unset": use `${SUGGEST_IGNORE+set}` test so an explicitly empty array from the user is respected and not overwritten by the default. ## lastupdate change (required) The current `lastupdate()` writes a human string: ``` Last update: ``` This is not machine-parseable for mtime comparison. Change `lastupdate()` to **also** write epoch seconds to a sibling file: - `${DEFAULT_OUTPUT_DIR}/lastupdate` — unchanged human-readable file (kept for display and backward compatibility). - `${DEFAULT_OUTPUT_DIR}/lastupdate.epoch` — new file containing a single integer: `date +%s`. The suggest logic reads `lastupdate.epoch`. If the file is absent or unparseable, the last-backup timestamp is treated as `0` (everything is new). This is the only change to existing backup behavior. ## Output When candidates exist: ``` New since last backup (): .config/foobar .newtool .somerc Paste into files.list: .config/foobar .newtool .somerc ``` - The indented top section is for human reading (colored like other output). - The bottom block is unindented, plain, ready to paste/append to `files.list`. - Candidates sorted alphabetically for stable output. When no candidates: ``` No new config dirs/files since last backup. ``` When `lastupdate.epoch` missing: - Still works; header reads `New since last backup (never):` and all unlisted, unignored paths are shown. ## Interaction with `--quiet` `--suggest` always writes to the real stdout, even when `--quiet` is also given. A suggest report in a log file is useless — the user needs to see paste-ready output on the terminal. Implementation: the quiet redirect (currently early in the script, saving the original stdout to fd 3) must **not** apply to suggest. Either: - skip the quiet redirect entirely when `SUGGEST == true`, OR - have `do_suggest` write to the saved fd 3. Skipping the redirect is simpler and preferred: `--suggest` exits before the backup flow anyway, so quiet has nothing else to suppress. Guard the redirect block with `if [[ "$QUIET" == true && "$SUGGEST" == false ]]`. ## Scope cuts (YAGNI) - No interactive prompt, no `y/n`, no appending to `files.list`. Print only. - No recursion deeper than one level in any scan source. - No mtime tracking per-path beyond the single last-backup epoch. - No new dependencies (pure bash + coreutils `date`, `stat`). ## Files touched - `dot-backup.sh`: - Add `SUGGEST=false` flag var + `-s|--suggest` case + usage line. - Modify `lastupdate()` to also write `lastupdate.epoch`. - Add `SUGGEST_IGNORE` default (guarded by unset test). - Add `do_suggest()` function (scan, filter, dedup, print). - Early dispatch: if `SUGGEST == true`, call `do_suggest` and exit before the backup/restore flow. - `config.example`: add commented `SUGGEST_IGNORE` example. - `README.md`: document the `--suggest` flag in the options/usage section. - `CLAUDE.md`: note the flag and the `lastupdate.epoch` file. ## Testing (manual) No test harness in repo. Manual verification steps: 1. `./dot-backup.sh --suggest` with no `lastupdate.epoch` → lists all unlisted, unignored hidden dirs/files and `.config` children. 2. Run a real backup, then `--suggest` → list shrinks to only paths newer than the backup. 3. `mkdir ~/.config/zzztest`, then `--suggest` → `zzztest` appears. 4. Add `.config/zzztest` to `files.list`, re-run `--suggest` → it disappears (dedup). 5. Add `zzztest` basename to `SUGGEST_IGNORE` in config (after removing from list) → disappears (ignore). 6. `--dry-run --suggest` and `--suggest --restore` → suggest wins, exits clean.