1 Commits
Author SHA1 Message Date
sneak a2fdb0b55d Rewrite README.md sentence by sentence for a newcomer
check / check (push) Canceled after 0s
The old README was one dense specification: long sentences, lists
chained with commas, exceptions ahead of the rules, and release
history in the user text. This rewrite leads with what the program is
and how to use it, then gives a reference for the settings, each
subcommand, the database, the output formats, the progress display
and the exit codes. Non-goals come before the reference; the
constraints, the entrypoints and the build come last. Every statement
was checked against the code on next and against the built binary on
small trees. The section names the code comments cite are kept. Five
comments in main_test.go cite Settings, and one each in scan_test.go,
trees_test.go and exclude_test.go cites what the README now says.
TODO.md records the step.

Model: fable-5-1
2026-10-26 16:15:49 +00:00
+26 -25
View File
@@ -69,9 +69,9 @@ The binary goes to `$(go env GOPATH)/bin`, or to `$GOBIN` when that is set.
`go install`, `VERSION` is the version Go recorded in it. For `@latest` that is
a release tag such as `v0.0.3`. For `@main` it is a pseudo-version, Go's version
string for an untagged commit. That string ends in the commit's hash. A binary
built from a clone carries the version `git describe --tags --always --dirty`
prints in that clone, such as `v0.0.3` on a tagged commit. `VERSION` is `dev`
when no version is known. To see everything Go recorded in an installed binary:
built from a clone carries the output of `git describe --tags --always --dirty`
in that clone, such as `v0.0.3` on a tagged commit. `VERSION` is `dev` when no
version is known. To see everything Go recorded in an installed binary:
```sh
go version -m "$(command -v sfdupes)"
@@ -105,8 +105,8 @@ docker run --rm -v /var/lib/sfdupes:/var/lib/sfdupes sfdupes report > dupes.tsv
### Daily scan from `cron`
Run `scan` as root, so that it can read every file. A path it cannot read is
skipped with a warning and loses its database record. As the file
Run `scan` as root, so that it can read every file. A path that it cannot read
is skipped with a warning and loses its database record. As the file
`/etc/cron.d/sfdupes`:
```
@@ -134,8 +134,8 @@ skipped with a warning and loses its database record. As the file
- A scan that fails exits 1, with the reason among the last lines of the log.
`tail` prints those lines, and `cron` mails them to root if the host can send
mail.
- A scan still running when the next one starts carries on. The new one fails at
once, and the lines `cron` mails include
- A running scan carries on when the next one starts. The new one fails at once,
and the lines that `cron` mails include
`sfdupes: another scan is running (lock held on /var/lib/sfdupes/db.sqlite.lock)`.
- `report`, `trees` and `info` need only read access to the database. The umask
is the setting that removes permissions from the files a process creates.
@@ -350,7 +350,7 @@ default. An operand may be a directory or a regular file.
"Concurrency").
- An operand that repeats another is dropped before the walk, without a line.
- An operand under another operand is dropped the same way, unless one of the
cases below applies. No file is then reached twice.
cases below applies. No path is then walked twice.
- An operand under another is kept, and walked on its own, when the inner
operand or a directory between the two is one of these:
- a default exclude (see "Rules for the walk");
@@ -602,7 +602,8 @@ signal `kill` sends by default:
- The records hashed but not yet written are written, so the next scan does not
hash them again.
- Nothing else is written or deleted. Records are deleted only after a complete
walk. So the records under paths an interrupted walk never reached are kept.
walk. So the records under paths that an interrupted walk never reached are
kept.
- The database is closed and the lock released, as on any other exit.
- The line `scan: interrupted after N files` goes to stderr. `N` is the number
of files the walk reached. The exit code is 1.
@@ -667,7 +668,7 @@ Four bytes are written as escapes:
Every other byte is written unchanged, including bytes that are not valid UTF-8,
the usual text encoding. Undoing the four escapes gives back the stored path.
Grouping and ordering use the stored path. The warnings `scan` prints are
Grouping and ordering use the stored path. The warnings that `scan` prints are
escaped the same way, so each warning stays one line.
The summary line on stderr gives:
@@ -800,7 +801,7 @@ All four subcommands work on one SQLite database file.
#### Location
The database is the file `SFDUPES_DATABASE` names, when that is set and not
The database is the file named by `SFDUPES_DATABASE`, when that is set and not
empty, and otherwise `/var/lib/sfdupes/db.sqlite`.
Each subcommand prints one line on stderr naming the database it uses, before
@@ -846,8 +847,8 @@ Only one `scan` runs against a database at a time.
the lock file is named the same way before the first scan as after. A symlink
at the database path is followed, and its target is created.
- A second `scan` against the same database does not wait. It fails at once with
a one-line error naming the lock file, and exits 1. It has walked nothing and
opened nothing. The running scan carries on.
an error naming the lock file, and exits 1. It has walked nothing and opened
nothing. The running scan carries on.
- The lock file is created on first use and never deleted. Only its owner may
open it. A leftover file blocks nothing. The lock ends with the process that
holds it, however that process ends. Deleting the file while a scan runs would
@@ -960,7 +961,7 @@ The phases, and what each display counts:
- `trees` has four phases:
- `[1/4] open` and `[2/4] read` are as for `report`, except that `read`
writes nothing.
- `[3/4] group` has the directories `read` found as its total.
- `[3/4] group` has the directories that `read` found as its total.
- `[4/4] write` has the duplicate-tree groups as its total.
- `info` has two phases:
- `[1/2] open` is as for `report`.
@@ -1036,9 +1037,9 @@ On a terminal:
- the elapsed time;
- the time remaining.
That happens hours into a scan of millions of files, on a terminal narrower
than about 75 columns. A terminal made narrower while a phase runs is fitted
at the next draw.
On a terminal narrower than about 75 columns, a line is cut short only hours
into a scan of millions of files. A terminal made narrower during a phase is
fitted at the next draw.
- A warning printed during a phase lands on a line of its own, never inside the
display. So does every line written to stdout when stdout and stderr are the
@@ -1082,9 +1083,9 @@ the count it stopped at.
- `--workers` below 1;
- `report`, `trees` or `info` with a positional argument.
A fatal error is reported in one line on stderr that starts with `sfdupes:`. A
failed write to stdout, such as a full disk, is reported the same way. Two cases
never reach `sfdupes` as a failed write:
A fatal error is reported on stderr, starting with `sfdupes:`. A failed write to
stdout, such as a full disk, is reported the same way. Two cases never reach
`sfdupes` as a failed write:
- The reader of a stdout pipe may exit early, as `head` does in
`sfdupes report | head`. The next write then ends `sfdupes` with `SIGPIPE`,
@@ -1163,8 +1164,8 @@ the repository root itself, so it can be run from any directory.
with `prettier`, in place. `prettier` indents by 4 spaces and wraps prose, as
`.prettierrc` sets and as the script also passes on the command line. It wraps
at 80 columns, its default width. Both run on the host. `prettier` runs
through `yarn` at the version `yarn.lock` pins. When the `node` on `PATH` is
not `node` 22, the script loads the `node` 22.17.0 that `script/bootstrap`
through `yarn` at the version that `yarn.lock` pins. When the `node` on `PATH`
is not `node` 22, the script loads the `node` 22.17.0 that `script/bootstrap`
installed.
- `script/fmt-check` is the read-only form of `script/fmt`. It names any
unformatted file and exits non-zero instead of writing. `gofmt` and `prettier`
@@ -1196,9 +1197,9 @@ install system packages and download the Go modules. Every step that checks or
compiles the code comes after it. Every `RUN` step after it runs again on each
build. Without that, Docker would serve the test and lint steps from its cache
on an unchanged tree, and the build would pass without running them. The steps
above `ARG CACHEBUST` stay cached until something they depend on changes, such
as `go.mod`. Go's build cache is kept between builds, so only what changed is
compiled again. The tests run with `-count=1`, so no test reports a stored
above `ARG CACHEBUST` stay cached until a change to something they depend on,
such as `go.mod`. Go's build cache is kept between builds, so only what changed
is compiled again. The tests run with `-count=1`, so no test reports a stored
result. `golangci-lint run` keeps the linter's own cache as well, so an
unchanged package is not linted again.