Test report and trees stdout write failures (closes #30)
check / check (push) Successful in 1m34s
check / check (push) Successful in 1m34s
report and trees already checked every stdout write and the final flush. run now takes the stdout it hands to them, so tests pass a closed file or a failing writer instead of swapping os.Stdout: a closed stdout exits 1 with a one-line diagnostic, and the writer's error reaches the caller. README "Error handling" now states the two cases that never reach sfdupes as a failed write: a pipe reader that exits early ends the process with SIGPIPE, as with cat; and stdout closed with >&- is replaced by /dev/null by the Go runtime before main runs, so the run succeeds. Model: opus-5-5
This commit was merged in pull request #74.
This commit is contained in:
@@ -57,22 +57,27 @@ var errNoSubcommand = errors.New("no subcommand")
|
||||
var Version = "dev"
|
||||
|
||||
func main() {
|
||||
os.Exit(run(os.Args[1:], os.Stderr))
|
||||
// Once the reader of a stdout pipe has gone, as in "sfdupes report |
|
||||
// head", the Go runtime ends the process with SIGPIPE on the next
|
||||
// write instead of returning an error (README "Error handling").
|
||||
// Registering for SIGPIPE with os/signal would change that.
|
||||
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
// run executes args against the command tree and returns the process
|
||||
// exit code. It is the program's single exit point: the subcommands
|
||||
// return their errors instead of exiting, so every deferred cleanup —
|
||||
// above all closing the database, which checkpoints the SQLite WAL —
|
||||
// runs before the process ends.
|
||||
func run(args []string, stderr io.Writer) int {
|
||||
// runs before the process ends. The report and trees subcommands write
|
||||
// their data to stdout.
|
||||
func run(args []string, stdout, stderr io.Writer) int {
|
||||
// A nil slice makes cobra fall back to os.Args, which would let a
|
||||
// test binary's own flags reach the command tree.
|
||||
if args == nil {
|
||||
args = []string{}
|
||||
}
|
||||
|
||||
root := newRootCommand(stderr)
|
||||
root := newRootCommand(stdout, stderr)
|
||||
root.SetArgs(args)
|
||||
|
||||
err := root.Execute()
|
||||
@@ -98,7 +103,7 @@ func run(args []string, stderr io.Writer) int {
|
||||
// newRootCommand builds the command tree. Everything on stdout is
|
||||
// machine-readable data; all human-facing output (help, usage, errors)
|
||||
// goes to stderr.
|
||||
func newRootCommand(stderr io.Writer) *cobra.Command {
|
||||
func newRootCommand(stdout, stderr io.Writer) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "sfdupes",
|
||||
Short: "Find candidate duplicate files by size and head/tail/content SHA-256",
|
||||
@@ -140,7 +145,7 @@ func newRootCommand(stderr io.Writer) *cobra.Command {
|
||||
Short: "Read the scan database and print the file-level duplicates report",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runE(func(ctx context.Context, _ []string) error {
|
||||
return runReport(ctx)
|
||||
return runReport(ctx, stdout)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -149,7 +154,7 @@ func newRootCommand(stderr io.Writer) *cobra.Command {
|
||||
Short: "Read the scan database and print the duplicate-tree report",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: runE(func(ctx context.Context, _ []string) error {
|
||||
return runTrees(ctx)
|
||||
return runTrees(ctx, stdout)
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user