1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
|
# 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/<name>`.
2. `~/.*/` — top-level hidden directories in `$HOME`.
Emitted as `.<name>`.
3. `~/.*` (files) — top-level hidden regular files in `$HOME`.
Emitted as `.<name>`.
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: <date>
```
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 (<human date of 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` output goes to stdout like normal. If combined with `--quiet`, it
follows the existing redirect (writes to log). No special handling — suggest is
a read-only report, quiet just redirects it. Document this is allowed but
unusual.
## 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.
|