From 520086189fc119e4e2617bb1e55a04c45e045aa0 Mon Sep 17 00:00:00 2001 From: alliasgher Date: Tue, 14 Apr 2026 03:41:30 +0500 Subject: [PATCH 1/2] errors: don't print an empty line for Exit with empty message When HandleExitCoder received an ExitCoder whose Error() returned an empty string (e.g. cli.Exit("", code)), it still called fmt.Fprintln(ErrWriter, err) which wrote a bare newline to stderr. Guard the print with a check on the error message so that an empty message produces no output at all. Fixes #2263 Signed-off-by: alliasgher --- errors.go | 10 ++++++---- errors_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/errors.go b/errors.go index f365a57990..ffd6471f23 100644 --- a/errors.go +++ b/errors.go @@ -150,10 +150,12 @@ func HandleExitCoder(err error) { } if exitErr, ok := err.(ExitCoder); ok { - if _, ok := exitErr.(ErrorFormatter); ok { - _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) - } else { - _, _ = fmt.Fprintln(ErrWriter, err) + if msg := err.Error(); msg != "" { + if _, ok := exitErr.(ErrorFormatter); ok { + _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + _, _ = fmt.Fprintln(ErrWriter, err) + } } OsExiter(exitErr.ExitCode()) return diff --git a/errors_test.go b/errors_test.go index 35aaab54d4..6a497c58f5 100644 --- a/errors_test.go +++ b/errors_test.go @@ -232,3 +232,28 @@ func TestErrRequiredFlags_Error(t *testing.T) { expectedMsg = "Required flag \"flag1\" not set" assert.Equal(t, expectedMsg, err.Error()) } + +// TestHandleExitCoder_EmptyMessage is a regression test for +// https://github.com/urfave/cli/issues/2263. +// HandleExitCoder must not print an empty line to ErrWriter when the ExitCoder +// message is empty (e.g. cli.Exit("", code)). +func TestHandleExitCoder_EmptyMessage(t *testing.T) { + called := false + + OsExiter = func(rc int) { + if !called { + called = true + } + } + ErrWriter = &bytes.Buffer{} + + defer func() { + OsExiter = fakeOsExiter + ErrWriter = fakeErrWriter + }() + + HandleExitCoder(Exit("", 2)) + + assert.True(t, called) + assert.Empty(t, ErrWriter.(*bytes.Buffer).String(), "expected no output for empty-message exit") +} From 5f284b84435ec649f76eb26397a49d6da1cdcded Mon Sep 17 00:00:00 2001 From: alliasgher Date: Wed, 15 Apr 2026 17:02:13 +0500 Subject: [PATCH 2/2] test: cover ErrorFormatter direct path in HandleExitCoder Adds TestHandleExitCoder_ErrorFormatterDirect to exercise the Fprintf branch in HandleExitCoder when an ExitCoder also implements ErrorFormatter and is passed in directly. The existing TestHandleExitCoder_ErrorFormatter routes through a multiError which goes down a different branch, so the direct path was not hit by any test (codecov patch). Signed-off-by: alliasgher --- errors_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/errors_test.go b/errors_test.go index 6a497c58f5..bb86ff56f0 100644 --- a/errors_test.go +++ b/errors_test.go @@ -257,3 +257,23 @@ func TestHandleExitCoder_EmptyMessage(t *testing.T) { assert.True(t, called) assert.Empty(t, ErrWriter.(*bytes.Buffer).String(), "expected no output for empty-message exit") } + +// TestHandleExitCoder_ErrorFormatterDirect verifies the ErrorFormatter branch +// (Fprintf path) is exercised when the ExitCoder is passed to HandleExitCoder +// directly with a non-empty message. Distinct from +// TestHandleExitCoder_ErrorFormatter above which routes through a multiError. +func TestHandleExitCoder_ErrorFormatterDirect(t *testing.T) { + called := false + OsExiter = func(rc int) { called = true } + ErrWriter = &bytes.Buffer{} + + defer func() { + OsExiter = fakeOsExiter + ErrWriter = fakeErrWriter + }() + + HandleExitCoder(&exitFormatter{code: 7}) + + assert.True(t, called) + assert.Contains(t, ErrWriter.(*bytes.Buffer).String(), "some other special") +}