package cli import ( "context" "errors" "fmt" "strings" "tsne.dev/hopper/internal/log" ) // errorVerboser is implemented by error types that can contribute extra // technical detail to failure reports under --verbose (e.g. *bunny.apiError // exposing verb/path/status/cause). reportFailures prints it indented below the // friendly message when verbose is enabled. type errorVerboser interface { ErrorVerbose() string } // usageError marks a parse or validation failure that should print a short // usage line and exit 2, distinct from a runtime failure (exit 1). type usageError struct { msg string } func (e usageError) Error() string { return e.msg } // exitError signals that the command has already emitted all its user-facing // output (e.g. a failure list followed by the summary) and the process should // simply exit with the given code, printing nothing further. This keeps the // summary as the last line. type exitError struct { code int } func (e exitError) Error() string { return fmt.Sprintf("exit status %d", e.code) } // reportFailures prints a batch of per-file failures to stderr under a headline, // using the same layout everywhere or the context's cancellation cause. // It returns true if it reported an error, false otherwise. func reportFailures(ctx context.Context, msg string, errs []error) bool { if len(errs) > 0 { sb := strings.Builder{} sb.WriteString(msg) for _, err := range errs { sb.WriteString("\n ") sb.WriteString(err.Error()) if log.Verbose() { var v errorVerboser if errors.As(err, &v) { sb.WriteString("\n ") sb.WriteString(v.ErrorVerbose()) } } } log.Error(sb.String()) return true } else if ctxErr := context.Cause(ctx); ctxErr != nil { log.Error(ctxErr.Error()) return true } else { return false } } // fileError attaches the storage path to an operation failure so the report can // name the offending file. It preserves the wrapped cause (e.g. the bunny API // error) in the chain so both friendly rendering (Error) and verbose detail // (ErrorVerbose) still work via errors.As. type fileError struct { path string err error } func (e fileError) Error() string { return e.path + ": " + e.err.Error() } func (e fileError) Unwrap() error { return e.err } type cancellationError struct { msg string } // CancellationError returns an error that wraps `context.Canceled` but // with a custom error message. func CancellationError(msg string) error { return cancellationError{msg} } func (e cancellationError) Error() string { return e.msg } func (e cancellationError) Unwrap() error { return context.Canceled }