From ef59b5af449c430b0cb843b493dc6528f7ebcdb1 Mon Sep 17 00:00:00 2001 From: alliasgher Date: Tue, 14 Apr 2026 03:39:33 +0500 Subject: [PATCH 1/2] flag: prevent BoolWithInverseFlag.String from panicking without a tab When FlagStringer returns a string without a tab character, strings.Index returns -1 and out[-1:] panics with "slice bounds out of range [-1:]". Guard against this by clamping the index to 0 when no tab is found. In that case the entire FlagStringer output is used as the suffix portion of the formatted string, matching the intent of callers who use a custom stringer without the usual tab layout. Fixes #2303 Signed-off-by: alliasgher --- flag_bool_with_inverse.go | 8 ++++++++ flag_bool_with_inverse_test.go | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index 272dd98fec..680fe2c64e 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -179,6 +179,14 @@ func (bif *BoolWithInverseFlag) String() string { prefix = "-" } + // Guard against a FlagStringer that returns a string without a tab (e.g. + // a custom stringer or the default stringer when the flag does not + // implement DocGenerationFlag). In that case treat the entire output as + // the tab-delimited suffix so the slice never goes out of bounds. + if i < 0 { + i = 0 + } + return fmt.Sprintf("%s[%s]%s%s", prefix, bif.inversePrefix(), bif.Name, out[i:]) } diff --git a/flag_bool_with_inverse_test.go b/flag_bool_with_inverse_test.go index 981494586b..fbb7e43ff8 100644 --- a/flag_bool_with_inverse_test.go +++ b/flag_bool_with_inverse_test.go @@ -520,3 +520,26 @@ func TestBoolWithInverseFlag_SatisfiesVisibleFlagInterface(t *testing.T) { _ = f.IsVisible() } + +// TestBoolWithInverseFlagStringNoPanicWithNoTabStringer is a regression test for +// https://github.com/urfave/cli/issues/2303. +// BoolWithInverseFlag.String() panicked with "slice bounds out of range [-1:]" +// when the FlagStringer returned a string without a tab character. +func TestBoolWithInverseFlagStringNoPanicWithNoTabStringer(t *testing.T) { + orig := FlagStringer + defer func() { FlagStringer = orig }() + + FlagStringer = func(f Flag) string { + return "no tab here" + } + + flag := &BoolWithInverseFlag{ + Name: "verbose", + } + + // Must not panic. + got := flag.String() + if !strings.Contains(got, "verbose") { + t.Errorf("expected String() to contain the flag name, got %q", got) + } +} From e02938c64fc8dc768ecc555d286c273d585e53b6 Mon Sep 17 00:00:00 2001 From: alliasgher Date: Wed, 15 Apr 2026 17:02:41 +0500 Subject: [PATCH 2/2] test: cover single-character name branch in BoolWithInverseFlag.String Splits TestBoolWithInverseFlagStringNoPanicWithNoTabStringer into multi-character and single-character sub-tests so the `-` vs `--` prefix branch added by the panic guard is fully covered. Signed-off-by: alliasgher --- flag_bool_with_inverse_test.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/flag_bool_with_inverse_test.go b/flag_bool_with_inverse_test.go index fbb7e43ff8..af8e9beb4f 100644 --- a/flag_bool_with_inverse_test.go +++ b/flag_bool_with_inverse_test.go @@ -533,13 +533,19 @@ func TestBoolWithInverseFlagStringNoPanicWithNoTabStringer(t *testing.T) { return "no tab here" } - flag := &BoolWithInverseFlag{ - Name: "verbose", - } + t.Run("multi-character name uses -- prefix", func(t *testing.T) { + flag := &BoolWithInverseFlag{Name: "verbose"} + got := flag.String() // must not panic + if !strings.Contains(got, "--[no-]verbose") { + t.Errorf("expected String() to contain --[no-]verbose, got %q", got) + } + }) - // Must not panic. - got := flag.String() - if !strings.Contains(got, "verbose") { - t.Errorf("expected String() to contain the flag name, got %q", got) - } + t.Run("single-character name uses - prefix", func(t *testing.T) { + flag := &BoolWithInverseFlag{Name: "v"} + got := flag.String() // must not panic + if !strings.Contains(got, "-[no-]v") { + t.Errorf("expected String() to contain -[no-]v, got %q", got) + } + }) }