Compare commits

..
Author SHA1 Message Date
sneak 4dba676880 Say "skipped" in scan's warnings for skipped paths (closes #170)
check / check (push) Successful in 3m10s
A warning for a path the summary counts as skipped now reads
`scan: skipped PATH (REASON)`, naming the path once. This covers
unreadable directories, failed lstats, failed reads and dropped
operands. Each path of a hard-linked file that cannot be read gets its
own line, since each is counted. Lines for paths left out on purpose
(`--exclude`, default excludes, filesystems that are not persistent)
now read `scan: left out PATH (...)`, so a search for `scan: skipped`
finds only the counted paths.

Model: opus-5-5
2026-11-11 12:08:27 +00:00
5 changed files with 39 additions and 59 deletions

No files matched your search

+6 -8
View File
@@ -255,7 +255,6 @@ In order:
time, against what the database holds.
4. **Keep the streams apart.** Everything on stdout is machine-readable data.
Everything for a human reader, such as progress and warnings, goes to stderr.
The one exception is help that was asked for, which goes to stdout.
### Subcommands
@@ -325,13 +324,12 @@ The reference below is also the program's specification.
"Operands").
- `--version`, `-v` prints `sfdupes VERSION` (see "Install") to stdout and
exits 0. It writes nothing to stderr.
- `--help`, `-h`, alone or after a subcommand, prints the help text to stdout
and exits 0. It writes nothing to stderr. `sfdupes help` and
`sfdupes help COMMAND` do the same. The help of `sfdupes` and of `scan` names
the database. The help of `report`, `trees`, `info` and `dwim` says what each
prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the default
excludes of the system they run on, and the filesystem types the walk never
records anything on.
- `--help`, `-h`, alone or after a subcommand, prints the help text to stderr
and exits 0. It writes nothing to stdout. The help of `sfdupes` and of `scan`
names the database. The help of `report`, `trees`, `info` and `dwim` says what
each prints. `sfdupes scan --help` and `sfdupes dwim --help` also list the
default excludes of the system they run on, and the filesystem types the walk
never records anything on.
### Duplicate detection
+3 -15
View File
@@ -201,9 +201,8 @@ func run(args []string, stdout, stderr io.Writer) int {
}
// newRootCommand builds the command tree. Everything on stdout is
// machine-readable data, the version line included, or help that was
// asked for. The rest of the human-facing output (usage, errors) goes
// to stderr.
// machine-readable data, the version line included; all human-facing
// output (help, usage, errors) goes to stderr.
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
var showVersion bool
@@ -236,22 +235,11 @@ func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
return errNoSubcommand
},
}
// Cobra prints the usage text of a usage error to the command's
// output writer, so that writer is stderr.
root.SetOut(stderr)
root.SetErr(stderr)
root.CompletionOptions.DisableDefaultCmd = true
// Cobra's default help also prints to the output writer. Help that
// was asked for, with --help, -h or the help subcommand, goes to
// stdout instead.
printHelp := root.HelpFunc()
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
cmd.SetOut(stdout)
printHelp(cmd, args)
})
// Cobra's built-in version flag prints to the output writer,
// Cobra's built-in version flag prints through the help writer,
// stderr; this one prints to stdout.
root.Flags().BoolVarP(&showVersion, "version", "v", false,
"print the version to stdout")
+21 -27
View File
@@ -26,9 +26,6 @@ const usageMarker = "Usage:"
// helpFlag asks for the help text.
const helpFlag = "--help"
// helpCommand is the subcommand that prints the help text.
const helpCommand = "help"
// workersFlag is the scan and dwim flag that sets the worker count.
const workersFlag = "--workers"
@@ -703,12 +700,9 @@ func TestRunScanRejectsWorkersBelowOne(t *testing.T) {
func TestRunHelp(t *testing.T) {
t.Parallel()
// README §Settings: help goes to stdout, exits 0, and leaves stderr
// README §Settings: help goes to stderr, exits 0, and leaves stdout
// empty.
cases := [][]string{
{helpFlag}, {"-h"}, {helpCommand},
{cmdScan, helpFlag}, {helpCommand, cmdScan},
}
cases := [][]string{{helpFlag}, {"-h"}, {cmdScan, helpFlag}}
for _, args := range cases {
var stdout, stderr bytes.Buffer
@@ -718,13 +712,14 @@ func TestRunHelp(t *testing.T) {
t.Errorf("run(%v) = %d, want %d", args, code, exitOK)
}
if !strings.Contains(stdout.String(), usageMarker) {
t.Errorf("run(%v) stdout = %q, want the help text",
args, stdout.String())
if !strings.Contains(stderr.String(), usageMarker) {
t.Errorf("run(%v) stderr = %q, want the help text",
args, stderr.String())
}
if got := stderr.String(); got != "" {
t.Errorf("run(%v) stderr = %q, want nothing", args, got)
if got := stdout.String(); got != "" {
t.Errorf("run(%v) stdout = %q, want nothing (data only)",
args, got)
}
// README §Location: the help of sfdupes and of scan says where
@@ -734,9 +729,9 @@ func TestRunHelp(t *testing.T) {
"scan and dwim store", databaseEnv, rootDatabasePath,
stateHomeEnv, "~/.local/state", "not an absolute path is an error",
} {
if !strings.Contains(stdout.String(), want) {
t.Errorf("run(%v) stdout = %q, want it to name %q",
args, stdout.String(), want)
if !strings.Contains(stderr.String(), want) {
t.Errorf("run(%v) stderr = %q, want it to name %q",
args, stderr.String(), want)
}
}
}
@@ -746,8 +741,8 @@ func TestRunHelp(t *testing.T) {
run([]string{cmdScan, "-h"}, &stdout, &stderr)
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
t.Errorf("scan -h stdout = %q, want %s PATH", stdout.String(),
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
t.Errorf("scan -h stderr = %q, want %s PATH", stderr.String(),
excludeFlag)
}
}
@@ -756,8 +751,7 @@ func TestRunScanUsageLine(t *testing.T) {
t.Parallel()
// README §Subcommands shows this usage line for scan, and the help
// and the usage error both print it. TestRunHelp and
// TestRunUsageErrors check which stream each goes to.
// and the usage error both print it.
const want = "\n sfdupes scan [--workers N] [-x] [--no-default-excludes] " +
"[--exclude PATH]... PATH...\n"
@@ -770,13 +764,13 @@ func TestRunScanUsageLine(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
var output bytes.Buffer
var stdout, stderr bytes.Buffer
run(args, &output, &output)
run(args, &stdout, &stderr)
if !strings.Contains(output.String(), want) {
t.Errorf("run(%v) output = %q, want the usage line %q",
args, output.String(), want)
if !strings.Contains(stderr.String(), want) {
t.Errorf("run(%v) stderr = %q, want the usage line %q",
args, stderr.String(), want)
}
})
}
@@ -842,9 +836,9 @@ func TestRunHelpDescribesOutput(t *testing.T) {
}
for _, want := range tc.want {
if !strings.Contains(stdout.String(), want) {
if !strings.Contains(stderr.String(), want) {
t.Errorf("run(%v) help lacks %q:\n%s",
args, want, stdout.String())
args, want, stderr.String())
}
}
}
+7 -7
View File
@@ -190,21 +190,21 @@ func TestRunDwimHelpListsScanFlags(t *testing.T) {
run([]string{cmdDwim, "-h"}, &stdout, &stderr)
if !strings.Contains(stdout.String(), want) {
t.Errorf("dwim -h stdout = %q, want the usage line %q",
stdout.String(), want)
if !strings.Contains(stderr.String(), want) {
t.Errorf("dwim -h stderr = %q, want the usage line %q",
stderr.String(), want)
}
if !strings.Contains(stdout.String(), excludeFlag+" PATH") {
t.Errorf("dwim -h stdout = %q, want %s PATH", stdout.String(),
if !strings.Contains(stderr.String(), excludeFlag+" PATH") {
t.Errorf("dwim -h stderr = %q, want %s PATH", stderr.String(),
excludeFlag)
}
// README "Settings": the help lists the default excludes and the
// filesystem types the walk never records anything on.
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
if !strings.Contains(stdout.String(), help) {
t.Errorf("dwim -h stdout = %q, want it to hold %q", stdout.String(),
if !strings.Contains(stderr.String(), help) {
t.Errorf("dwim -h stderr = %q, want it to hold %q", stderr.String(),
help)
}
}
+2 -2
View File
@@ -196,8 +196,8 @@ func TestDefaultExcludesHelp(t *testing.T) {
run([]string{cmdScan, "-h"}, &stdout, &stderr)
help := strings.TrimSpace(defaultExcludesHelp(runtime.GOOS))
if !strings.Contains(stdout.String(), help) {
t.Errorf("scan --help = %q, want it to hold %q", stdout.String(), help)
if !strings.Contains(stderr.String(), help) {
t.Errorf("scan --help = %q, want it to hold %q", stderr.String(), help)
}
}