diff --git a/flag.go b/flag.go index e8cecc9..57fdb95 100644 --- a/flag.go +++ b/flag.go @@ -704,6 +704,13 @@ func wrapN(i, slop int, s string) (string, string) { w := strings.LastIndexAny(s[:i], " \t\n") if w <= 0 { + // There is nowhere to break within the first `i` runes, so the + // leading word is wider than the line. Let that one word overflow + // and break after it, rather than returning all of `s` and leaving + // everything that follows unwrapped. + if x := strings.IndexAny(s[i:], " \t\n"); x >= 0 { + return s[:i+x], s[i+x+1:] + } return s, "" } nlPos := strings.LastIndex(s[:i], "\n") diff --git a/printusage_test.go b/printusage_test.go index 076d13d..d4de3b8 100644 --- a/printusage_test.go +++ b/printusage_test.go @@ -3,6 +3,7 @@ package pflag import ( "bytes" "io" + "strings" "testing" ) @@ -72,3 +73,60 @@ func TestPrintUsage_2(t *testing.T) { t.Errorf("Expected \n%s \nActual \n%s", expectedOutput2, res) } } + +const expectedOutput3 = ` --mount string a mount specification e.g. + 'type=bind,source=/opt,destination=/hostopt'. + The rest of this description is + wrapped. +` + +// A word wider than the description column has to overflow, but only that +// word: wrapping resumes on the next one. +func TestPrintUsage_WrapsAfterUnbreakableWord(t *testing.T) { + f := NewFlagSet("test", ExitOnError) + f.String("mount", "", "a mount specification e.g. 'type=bind,source=/opt,destination=/hostopt'. The rest of this description is wrapped.") + + res := f.FlagUsagesWrapped(60) + if res != expectedOutput3 { + t.Errorf("Expected \n%s \nActual \n%s", expectedOutput3, res) + } +} + +// No line may be longer than the requested width unless it is a single word +// that cannot be broken at all. +func TestPrintUsage_OnlyUnbreakableWordsOverflow(t *testing.T) { + usages := []string{ + "a mount specification e.g. 'type=bind,source=/opt,destination=/hostopt'. The rest of this description is wrapped.", + "https://example.com/a/very/long/url/that/will/not/fit/in/the/column and some words after it to wrap", + "averyveryverylongwordrightatthestart followed by several shorter words", + "short words then averyveryverylongwordinthemiddle and more words after that one", + "trailing text ending in anextremelylongunbreakabletokenattheveryend", + } + + for _, width := range []int{40, 60, 80} { + for _, usage := range usages { + f := NewFlagSet("test", ExitOnError) + f.String("mount", "", usage) + + lines := strings.Split(strings.TrimRight(f.FlagUsagesWrapped(width), "\n"), "\n") + if len(lines) < 2 { + t.Errorf("width %d, usage %q: expected the description to wrap", width, usage) + continue + } + + // Continuation lines are padded to the description column, so + // their indent says where the description starts on every line. + indent := len(lines[1]) - len(strings.TrimLeft(lines[1], " ")) + + for _, line := range lines { + if len(line) <= width || indent >= len(line) { + continue + } + if strings.Contains(strings.TrimSpace(line[indent:]), " ") { + t.Errorf("width %d, usage %q: line is %d wide and still has a break point:\n%s", + width, usage, len(line), line) + } + } + } + } +}