From c824838f63f71e52e1a08d2aea981d9e80f95caf Mon Sep 17 00:00:00 2001 From: Januth Nimnal Date: Tue, 29 Sep 2026 00:11:52 +0530 Subject: [PATCH 1/2] Use prefixFor so suggestions agree with help on rune count --- suggestions.go | 11 ++++----- suggestions_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/suggestions.go b/suggestions.go index 401dcef253..4c1ce9aadb 100644 --- a/suggestions.go +++ b/suggestions.go @@ -120,13 +120,14 @@ func suggestFlag(flags []Flag, provided string, hideHelp bool) string { } } - if len(suggestion) == 1 { - suggestion = "-" + suggestion - } else if len(suggestion) > 1 { - suggestion = "--" + suggestion + if len(suggestion) == 0 { + return "" } - return suggestion + // Use the same rune-counting rule that drives the help output + // (prefixFor) so that a single-rune name is suggested as a short flag + // regardless of how many bytes that rune is encoded in. + return prefixFor(suggestion) + suggestion } // suggestCommand takes a list of commands and a provided string to suggest a diff --git a/suggestions_test.go b/suggestions_test.go index b1e962104e..03a3b4d349 100644 --- a/suggestions_test.go +++ b/suggestions_test.go @@ -3,6 +3,7 @@ package cli import ( "errors" "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -56,6 +57,60 @@ func TestSuggestFlag(t *testing.T) { } } +// TestSuggestFlagMultibyteRunePrefix ensures the "-"/"--" prefix chosen for a +// suggested flag name is derived from the rune count, exactly like the prefix +// used when the very same flag is rendered in help output by prefixFor. A +// single (non-ASCII) rune flag such as "é" is a short flag and must be +// suggested as "-é", not "--é". +func TestSuggestFlagMultibyteRunePrefix(t *testing.T) { + for _, testCase := range []struct { + name, provided, expected string + }{ + // Single ASCII rune: short flag. + {"single-rune-ascii", "ss", "-s"}, + // Single multi-byte rune: still a short flag. + {"single-rune-multibyte", "éé", "-é"}, + } { + t.Run(testCase.name, func(t *testing.T) { + fl := &BoolFlag{Name: "é"} + if testCase.name == "single-rune-ascii" { + fl = &BoolFlag{Name: "s"} + } + + // The suggestion must agree with how the flag is advertised in + // help output, which is driven by prefixFor. + assert.Equal(t, "-"+fl.Names()[0], flagStringForTest(fl)) + + assert.Equal(t, testCase.expected, suggestFlag([]Flag{fl}, testCase.provided, true)) + }) + } +} + +// TestSuggestFlagMultibyteRuneFromError covers the user-visible output: the +// "Did you mean ...?" hint must name the short flag exactly as help output +// does. +func TestSuggestFlagMultibyteRuneFromError(t *testing.T) { + cmd := &Command{Flags: []Flag{&BoolFlag{Name: "é"}}} + + res, err := cmd.suggestFlagFromError( + errors.New(providedButNotDefinedErrMsg+"éé"), + "", + ) + assert.NoError(t, err) + assert.Equal(t, fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", "-é"), res) + assert.Equal(t, "Did you mean \"-é\"?\n\n", res) +} + +// flagStringForTest returns the leading flag-name portion of a flag's help +// rendering, e.g. "-é" or "--verbose". +func flagStringForTest(fl Flag) string { + s := stringifyFlag(fl) + if i := strings.IndexByte(s, '\t'); i >= 0 { + return strings.TrimSpace(s[:i]) + } + return strings.TrimSpace(s) +} + func TestSuggestFlagHideHelp(t *testing.T) { // Given app := buildExtendedTestCommand() From bc3e33633b1439e1e1ada895033c684d722c0c7e Mon Sep 17 00:00:00 2001 From: Eng Zer Jun Date: Tue, 29 Sep 2026 14:14:34 +0800 Subject: [PATCH 2/2] test: reduce suggestFlag single-rune Unicode coverage to one case Replace the two new tests and the flagStringForTest helper with a case in TestSuggestFlag. prefixFor with a single-rune Unicode name is tested by TestCommand_SingleRuneUnicodeFlag, and the suggestFlagFromError path is covered by TestSuggestFlagFromError. Signed-off-by: Eng Zer Jun --- suggestions_test.go | 57 ++------------------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/suggestions_test.go b/suggestions_test.go index 03a3b4d349..67fe3c1b4a 100644 --- a/suggestions_test.go +++ b/suggestions_test.go @@ -3,7 +3,6 @@ package cli import ( "errors" "fmt" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -39,6 +38,7 @@ func TestJaroWinkler(t *testing.T) { func TestSuggestFlag(t *testing.T) { // Given app := buildExtendedTestCommand() + app.Flags = append(app.Flags, &BoolFlag{Name: "é"}) for _, testCase := range []struct { provided, expected string @@ -48,6 +48,7 @@ func TestSuggestFlag(t *testing.T) { {"hlp", "--help"}, {"k", ""}, {"s", "-s"}, + {"éé", "-é"}, } { // When res := suggestFlag(app.Flags, testCase.provided, false) @@ -57,60 +58,6 @@ func TestSuggestFlag(t *testing.T) { } } -// TestSuggestFlagMultibyteRunePrefix ensures the "-"/"--" prefix chosen for a -// suggested flag name is derived from the rune count, exactly like the prefix -// used when the very same flag is rendered in help output by prefixFor. A -// single (non-ASCII) rune flag such as "é" is a short flag and must be -// suggested as "-é", not "--é". -func TestSuggestFlagMultibyteRunePrefix(t *testing.T) { - for _, testCase := range []struct { - name, provided, expected string - }{ - // Single ASCII rune: short flag. - {"single-rune-ascii", "ss", "-s"}, - // Single multi-byte rune: still a short flag. - {"single-rune-multibyte", "éé", "-é"}, - } { - t.Run(testCase.name, func(t *testing.T) { - fl := &BoolFlag{Name: "é"} - if testCase.name == "single-rune-ascii" { - fl = &BoolFlag{Name: "s"} - } - - // The suggestion must agree with how the flag is advertised in - // help output, which is driven by prefixFor. - assert.Equal(t, "-"+fl.Names()[0], flagStringForTest(fl)) - - assert.Equal(t, testCase.expected, suggestFlag([]Flag{fl}, testCase.provided, true)) - }) - } -} - -// TestSuggestFlagMultibyteRuneFromError covers the user-visible output: the -// "Did you mean ...?" hint must name the short flag exactly as help output -// does. -func TestSuggestFlagMultibyteRuneFromError(t *testing.T) { - cmd := &Command{Flags: []Flag{&BoolFlag{Name: "é"}}} - - res, err := cmd.suggestFlagFromError( - errors.New(providedButNotDefinedErrMsg+"éé"), - "", - ) - assert.NoError(t, err) - assert.Equal(t, fmt.Sprintf(SuggestDidYouMeanTemplate+"\n\n", "-é"), res) - assert.Equal(t, "Did you mean \"-é\"?\n\n", res) -} - -// flagStringForTest returns the leading flag-name portion of a flag's help -// rendering, e.g. "-é" or "--verbose". -func flagStringForTest(fl Flag) string { - s := stringifyFlag(fl) - if i := strings.IndexByte(s, '\t'); i >= 0 { - return strings.TrimSpace(s[:i]) - } - return strings.TrimSpace(s) -} - func TestSuggestFlagHideHelp(t *testing.T) { // Given app := buildExtendedTestCommand()